Skip to main content

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']})")
The response envelope

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))