Assets in Agility
Assets are the fundamental building blocks of Digital.ai Agility. Every work item, project, sprint and configuration element is an asset with a unique identifier, a type, and a set of attributes.
What Is an Asset?
An asset is any entity stored in the Agility system:
- Work items: Stories, Defects, Test Sets, Tasks, Tests
- Portfolio items: Epics (displayed as Portfolio Item), Themes (displayed as Backlog Group)
- Organization and planning: Scopes (Projects), Timeboxes (Iterations/Sprints), Teams, Members
- Tracking: Issues, Requests, Goals
Each asset has:
- A unique Object ID (OID) such as
Story:1207 - An asset type that defines its structure and behaviour
- A set of attributes that store its data, including relations to other assets
The Asset Type Hierarchy
Asset types inherit from one another. The work-item corner of the hierarchy:
Two things about this tree surprise people:
Issueis not a work item. It descends directly fromBaseAsset, so aWorkitemquery never returns Issues.EpicandThemeare. Both descend fromWorkitem, so aWorkitemquery returns portfolio items alongside stories and tasks.
Discover any type's parent with the Meta API: GET /meta.v1/Story reports
Base: PrimaryWorkitem.
The model describes itself: asset types and their attribute definitions are
assets too. AttributeDefinition is a queryable type, and filtering it on
its AssetType relation returns a type's attribute definitions through
query.v1, rest-1.v1 or api/asset. It works, but it is not the
recommended path — meta.v1 serves the same information already shaped for
the purpose: inheritance resolved, operations included, no authentication
required.
Querying Abstract Types Is Allowed — and Useful
Workitem and PrimaryWorkitem are abstract: no asset is ever just a
Workitem, and you cannot create one. But you can absolutely query them,
and it is the natural way to work across subtypes:
{
"from": "PrimaryWorkitem",
"select": ["Number", "Name"],
"page": { "size": 50, "start": 0 }
}
That one query returns Stories, Defects and Test Sets together — the classic
"whole backlog" shape. Remember the tree when interpreting results: a
Workitem query includes Epics and Themes too.
Object IDs (OIDs)
Every asset has a unique Object ID: the asset type and a numeric id, joined by a colon.
Story:1207
Defect:5678
Scope:0
Member:20
An OID with a third part (Story:1207:5837) identifies a past version —
a moment — of the asset. See
Moments and history.
OID Vs Number
Work items also have a human-readable Number:
| Property | Example | Description |
|---|---|---|
| OID | Story:1207 | Permanent identifier — use in API calls |
| Number | S-01001 | Display number — use with humans |
| Name | Logon | Title |
Find an asset's OID from its Number (every result carries _oid):
{
"from": "Story",
"where": { "Number": "S-01001" },
"select": ["Number", "Name"]
}
Fetch a specific asset when you already hold its OID — filter on ID, the
attribute every asset has that refers to itself:
{
"from": "Story",
"where": { "ID": "Story:1207" },
"select": ["Number", "Name"]
}
Asset State and Status — Two Different Things
Two mechanisms track "where an asset is", and conflating them causes real bugs:
AssetStateis the system lifecycle: Future (0), Active (64), Closed/Inactive (128), and the dead states (192 and above, including Deleted at 255). Every asset has it, it is not customizable, and it is whatInactivate/Reactivateoperations change. See the state table in the quickstart.Statusis the workflow position — a list value like Future, In Progress, Done — customizable per instance and per project. See Workflows and status.
A story with Status = Done is usually still AssetState = Active; it
becomes Closed when someone closes it.
Lifecycle transitions are performed with operations, invoked as a query-string parameter with an empty POST body:
curl -X POST -H "Authorization: Bearer ${AGILITY_ACCESS_TOKEN}" \
"${AGILITY_BASE_URL}/rest-1.v1/Data/Story/1207?op=Inactivate"
See Operations for the catalogue and their quirks.
Relations Between Assets
Relations are attributes whose values are other assets. They come in two shapes:
- Single-value:
Scope,Timebox,Status,Parent,Super - Multi-value:
Owners,Children,Dependencies,Dependants
Frequently used work-item relations:
| Relation | Cardinality | Description |
|---|---|---|
Scope | single | The project that contains the asset |
Timebox | single | Sprint/iteration assignment |
Owners | multi | Assigned members — note the plural |
Children | multi | A primary workitem's Tasks and Tests |
Parent | single | A Task's or Test's primary workitem |
Super | single | A primary workitem's Epic (portfolio parent) |
Dependencies | multi | Upstream work this item depends on |
Traverse relations in a selection with dotted attribute paths:
{
"from": "Story",
"select": ["Name", "Scope.Name", "Timebox.Name", "Owners.Name"],
"page": { "size": 50, "start": 0 }
}
See Relations for traversal depth, filtering through relations, and downcasting.
Querying Assets
The response to a query.v1 request is an array of result arrays — one
per query in the request, each containing the matching assets:
[
[
{ "_oid": "Story:1207", "Number": "S-01001", "Name": "Logon" },
{ "_oid": "Story:1208", "Number": "S-01002", "Name": "Setup" }
]
]
Filter with where (exact match) and filter (full comparison grammar,
including OR-lists):
{
"from": "Story",
"where": { "Scope": "Scope:0" },
"filter": ["Status.Name='Future','Done'"],
"select": ["Number", "Name", "Status.Name"],
"page": { "size": 50, "start": 0 }
}
See the Query API and the query grammar for the full syntax.
Best Practices
- Use OIDs in code, Numbers with people.
- Query abstract types (
PrimaryWorkitem) to work across subtypes; remember you cannot create them. - Always page list queries — Agility has no default page size. See query size and performance.
- Select only what you need; every relation you project costs a join.
- Check
meta.v1when an attribute or type surprises you — the schema, including custom attributes, differs per instance.
Code Examples
TypeScript: Fetch One Asset by OID
async function fetchAsset(client: AgilityClient, oid: string) {
const [results] = await client.query({
from: oid.split(':')[0],
where: { ID: oid },
select: ['Number', 'Name'],
});
if (results.length === 0) {
throw new Error(`Asset ${oid} not found`);
}
return results[0];
}
const story = await fetchAsset(client, 'Story:1207');
console.log(`${story.Number}: ${story.Name}`);
Python: OID Helpers
def parse_oid(oid: str) -> tuple[str, int]:
"""Split an OID into asset type and numeric id."""
asset_type, _, num = oid.partition(':')
return asset_type, int(num)
def build_oid(asset_type: str, asset_id: int) -> str:
return f"{asset_type}:{asset_id}"
asset_type, asset_id = parse_oid("Story:1207")
Related Topics
- Attributes — attribute types and definitions
- Relations — traversal and filtering through the graph
- Moments and history — past versions and
asof - Operations — actions that change lifecycle state