Python Examples
The examples below form one sequence — the same operations appear in cURL, Python and TypeScript — so you can switch languages without switching mental models. Set up authentication first.
Setup
import os
import requests
BASE_URL = os.environ["AGILITY_BASE_URL"]
session = requests.Session()
session.headers.update({
"Authorization": f"Bearer {os.environ['AGILITY_ACCESS_TOKEN']}",
"Content-Type": "application/json",
"Accept": "application/json",
})
def query(payload: dict) -> list:
response = session.post(f"{BASE_URL}/query.v1", json=payload, timeout=30)
response.raise_for_status()
return response.json()[0] # first (and here only) query's results
Who Am I?
The best connection test — it proves the URL and the credentials, and tells you which member the token acts as:
me = query({
"from": "Member",
"where": {"IsSelf": True},
"select": ["Name", "Nickname", "Email"],
})[0]
print(f"Authenticated as {me['Name']} ({me['_oid']})")
query.v1 returns an array of result arrays — one inner array per query
in the request. There is no results or query wrapper object:
[
[
{ "_oid": "Story:1207", "Number": "S-01001", "Name": "Logon" }
]
]
Get One Story by Number
story = query({
"from": "Story",
"where": {"Number": "S-01001"},
"select": ["Number", "Name", "Description", "Status.Name"],
})[0]
List a Backlog, Paged Correctly
Page until a response comes back shorter than the page size — no total
needed. Scope.ParentMeAndUp means "in this project or any project beneath
it":
def backlog(scope_oid: str, size: int = 50):
start = 0
while True:
page = query({
"from": "PrimaryWorkitem",
"filter": [f"Scope.ParentMeAndUp='{scope_oid}'"],
"select": ["Number", "Name", "Status.Name"],
"sort": ["Number"],
"page": {"size": size, "start": start},
})
yield from page
if len(page) < size:
break
start += size
for item in backlog("Scope:0"):
print(item["Number"], item["Name"])
Filter Precisely
Quote filter values, compare relations by OID, and prefer RollupState
and UTC dates for queries that survive renames and timezones:
active = query({
"from": "Story",
"where": {"Scope": "Scope:0"},
"filter": ["Estimate>'2'", "Status.RollupState='Active'"],
"select": ["Number", "Name", "Estimate", "Status.Name"],
"sort": ["-ChangeDateUTC"],
"page": {"size": 50, "start": 0},
})
Traverse Relations
Dotted paths flatten into their own keys; a nested subquery groups child attributes into objects:
detail = query({
"from": "Story",
"where": {"Number": "S-01001"},
"select": [
"Name", "Scope.Name", "Owners.Name",
{"from": "Children:Task", "select": ["Number", "Name", "Status.Name"]},
],
})[0]
print(detail["Scope.Name"]) # flattened key
for task in detail["Children:Task"]: # grouped objects
print(" ", task["Number"], task["Name"])
Create and Update with the Bulk API
One surface for writes; the response lists what was created, modified and what failed:
def bulk(commands: list) -> dict:
response = session.post(f"{BASE_URL}/api/asset", json=commands, timeout=60)
response.raise_for_status()
return response.json()
created = bulk([{"AssetType": "Story", "Name": "My new story", "Scope": "Scope:0"}])
story_oid = created["assetsCreated"]["oidTokens"][0].rsplit(":", 1)[0] # strip the moment
bulk([{"from": story_oid, "update": {"Estimate": 3, "Owners": {"add": ["Member:20"]}}}])
Several commands can share one request, and later commands can reference earlier ones by alias:
bulk([
{"@epic": {"AssetType": "Epic", "Name": "New initiative", "Scope": "Scope:0"}},
{"@story": {"AssetType": "Story", "Name": "First slice", "Scope": "Scope:0", "Super": "@epic"}},
])
Run an Operation
Lifecycle changes are operations on rest-1.v1 — query parameter, empty
body:
session.post(f"{BASE_URL}/rest-1.v1/Data/Story/3884", params={"op": "Inactivate"})
Handle Failures Properly
A Bulk API request answers 200 even when commands fail — always check
commandFailures, and resolve violation tags to human-readable text
through the localizer (unmatched tags echo back unchanged):
result = bulk([{"from": "Story:3884", "update": {"Name": "renamed"}}])
if result["commandFailures"]["count"] > 0:
tag = result["commandFailures"]["commands"][0]["error"]["message"]
readable = session.get(f"{BASE_URL}/loc-2.v1?[{tag}]").json()
print(readable.get(tag, tag))
Related Topics
- Query size and performance — paging and projection discipline
- Bulk API — the full command reference
- Error conventions — including localizable violation tags