---
id: python
title: 'Python Examples'
description: 'Runnable Python examples for the Digital.ai Agility API'
sidebar_label: 'Python'
sidebar_position: 2
tags: ['examples', 'python']
---

The examples below form one sequence — the same operations appear in
[cURL](/examples/curl), [Python](/examples/python) and
[TypeScript](/examples/typescript) — so you can switch languages without
switching mental models. Set up
[authentication](/getting-started/authentication) first.

## Setup

```python
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:

```python
me = query({
    "from": "Member",
    "where": {"IsSelf": True},
    "select": ["Name", "Nickname", "Email"],
})[0]
print(f"Authenticated as {me['Name']} ({me['_oid']})")
```

:::note 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:

```json
[
  [
    { "_oid": "Story:1207", "Number": "S-01001", "Name": "Logon" }
  ]
]
```

:::

## Get One Story by Number

```python
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":

```python
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:

```python
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:

```python
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:

```python
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:

```python
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:

```python
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):

```python
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](/guides/query-size-and-performance) — paging and projection discipline
- [Bulk API](/apis/asset-bulk) — the full command reference
- [Error conventions](/apis/conventions/errors) — including localizable violation tags
