Workflows and Status
In Agility's API, a "workflow" is a list of status values that work items move through — not a state machine. The API places no restrictions on which status you set next; boards and team process impose order, the server does not. What the server does maintain is each status's rollup category, which quietly drives every report.
Which Types Use Which Status List
Status lists are shared across related types — verified from the schema:
| Work item type | Status list type |
|---|---|
| Story, Defect, TestSet | StoryStatus — one shared list for all primary workitems |
| Task | TaskStatus |
| Test | TestStatus |
| Epic | EpicStatus |
There is no DefectStatus: a Defect's Status dropdown draws from the same
StoryStatus list as Stories. Rename or reorder a value and every primary
workitem type sees the change.
Anatomy of a Status Value
Status values are assets. In the schema their types descend from an
abstract asset type named List (StoryStatus → Status → List) —
a name that reads backwards at first: each individual value is-a
List, so the type really means list entry. The list as a whole has
no asset of its own — "the StoryStatus list" is simply every asset of
type StoryStatus, optionally narrowed by a
team's process or a scheme.
When meta.v1 shows a relation whose target descends
from List, read it as "picks one entry from a value list".
The attributes that matter:
| Attribute | Type | Notes |
|---|---|---|
Name | Text | Instance data — editable, so code should match on OID, not name |
Order | Rank | Opaque ordering token: sort by it, never read it |
Description | LongText | |
RollupState | State | The status's lifecycle category: Future, Active or Closed |
Discover a list, in display order:
{
"from": "StoryStatus",
"select": ["Name", "RollupState"],
"sort": ["Order"],
"page": { "size": 50, "start": 0 }
}
RollupState
RollupState exists on StoryStatus and EpicStatus — Task and Test
statuses have no rollup, so effective-state reasoning applies to primary
workitems and Epics, not to their breakdown items. Where it exists, every
status value carries one — and it does not always match
what the name suggests. A live 26.2 sample:
| Status name | RollupState |
|---|---|
| Future | Future |
| In Progress | Active |
| Testing | Active |
| In UAT | Closed |
| Done | Closed |
| Accepted | Closed |
That In UAT → Closed row is the kind of configuration that makes an integration's numbers disagree with the product's: reporting treats an Active asset as effectively closed once its status rolls up as Closed. If your counts don't match a dashboard, this is the first place to look — see reproducing reported numbers.
RollupState also drives the DoneDate / DoneDateUTC synthetic
attributes (read-only): they are stamped the moment a workitem's effective
state moves to Closed — verified live: setting Status to a Closed-rolling
value populated DoneDateUTC immediately, while AssetState remained
Active. If you report cycle time, this is your completion timestamp — not
the moment someone ran Inactivate.
There is no StartedDate — but you can reconstruct it (and who caused it)
with a historical projection over the asset's history, filtering for the
moment the effective state moved from Future to Active. It works, verified
live, but it is costly — use it for targeted assets, not instance-wide
sweeps:
{
"from": "Story",
"where": { "ID": "Story:1207" },
"select": [
"Name",
"History[Prior.EffectiveAssetState='Future';EffectiveAssetState='Active'].ChangeDateUTC",
"History[Prior.EffectiveAssetState='Future';EffectiveAssetState='Active'].ChangedBy.Name"
]
}
(The semicolon inside the bracket filter is AND; History and Prior reach
across moments.)
It is often less costly to skip the filter and pull the workitem's whole
Status/AssetState history in one go, finding the transitions client-side.
History works as a subquery, returning one object per moment:
{
"from": "Story",
"where": { "ID": "Story:1207" },
"select": [
"Name",
{ "from": "History", "select": ["ChangeDateUTC", "Status.Name", "AssetState"] }
]
}
"History": [
{ "_oid": "Story:1207:1482", "ChangeDateUTC": "2026-05-25T…", "Status.Name": "Future", "AssetState": "Active" },
{ "_oid": "Story:1207:1605", "ChangeDateUTC": "2026-05-26T…", "Status.Name": "Future", "AssetState": "Active" }
]
Each entry's _oid carries the moment (Story:1207:1482) — see
Moments and history.
RollupState is also directly filterable, which gives you
process-independent queries that survive status renames:
{
"from": "Story",
"filter": ["Status.RollupState='Active'"],
"select": ["Number", "Name", "Status.Name"],
"page": { "size": 50, "start": 0 }
}
Changing Status
A status change is an ordinary attribute update — see Operations for the payload shapes on each write surface. Three facts to build on:
-
No transition order is enforced — Future to Accepted in one write is accepted. But value membership is: a team using a Team Process has its own set of
StoryStatusvalues its primary workitems may be in, and that set is enforced on status change — verified live. Writing a status outside the team's process fails withViolation'PrimaryWorkitemCannotHaveInvalidStatus. (Status values carry aTeamrelation — a team-scoped value belongs to that team's process.)The process itself is queryable, though the attribute names are not guessable:
Team.HasProcesssays whether the team enforces one, andTeam.SelectedValuesis the allowed set.{ "from": "Team", "select": ["Name", "HasProcess", { "from": "SelectedValues", "select": ["Name"] }] }A team with
HasProcess: "False"has an emptySelectedValuesand enforces nothing. A status value belongs to a team's process or to a project scheme, never both — creating one with both aTeamandSelectedInSchemesfails withViolation'ListValueCannotBelongToBothSchemeAndTeam. To ask the same question for one specific workitem — with every narrowing already applied — useapi/ValidValuesinstead. -
Use OIDs. Status names are instance data and freely editable.
-
Status is not lifecycle. Setting a Closed-rolling status does not close the asset;
AssetStatechanges only through operations likeInactivate. An asset can beStatus= Done andAssetState= Active indefinitely — see Assets.
Per-Project and per-Team Availability
Two mechanisms narrow which status values actually apply to a given work item — discover per context rather than assuming one universal set:
- Scheme — shown in the UI as the Project Workspace — controls which
values a project offers (status values carry a
SelectedInSchemesrelation). - Team Process — a team can have its own set of
StoryStatusvalues for its primary workitems, enforced on status change. Two related concepts ride along but are advisory only — the UI calls them out, the API does not enforce them: the team's WIP cycle (the subset of its statuses whoseRollupStateis Active) and its WIP limit (a cap on workitems in progress). Epics are not subject to team processes.
What Agility Offers Instead of Enforcement
The product covers the workflow-enforcement need with softer mechanisms an integration should know about:
- Important fields (soft requirements). A Scheme can flag attributes as
important (
Scheme.ImportantFields): the UI calls them out, and withScheme.IsRequiredAtClosethey become mandatory when the asset is closed — so a close can fail validation even though ordinary updates never did. Theapi/canbeimportantendpoint reports which attributes are eligible. - Closed assets accept backfill — and only backfill. A Closed asset
takes an attribute update without being reopened only if the attribute
had no value; changing an existing value fails with
Violation'ClosedAssetCannotEditExistingValues(both verified live — including that a value backfilled after close immediately becomes uneditable itself). The design intent is pushing missing values, not editing history. - Quick Transitions — follow-up actions configured in the UI that run when an asset reaches a new Status or AssetState. They are product configuration, not part of the API surface.
Useful Queries
// everything still genuinely open, by rollup rather than name
{ "from": "PrimaryWorkitem",
"filter": ["Status.RollupState!='Closed'", "AssetState='Active'"],
"select": ["Number", "Name", "Status.Name"],
"page": { "size": 100, "start": 0 } }
// stale in-flight work: active rollup, untouched for 30 days
{ "from": "Story",
"filter": ["Status.RollupState='Active'", "ChangeDateUTC<'2026-07-27'"],
"select": ["Number", "Name", "Status.Name", "ChangeDateUTC"],
"sort": ["ChangeDateUTC"],
"page": { "size": 50, "start": 0 } }
Related Topics
-
Product feature: Team Process management and What impact does defining Team Process have on my instance? — configuring the processes whose enforcement this page describes.
-
Assets — AssetState vs Status
-
Operations — how lifecycle actually changes
-
Reproducing reported numbers — RollupState in reporting
-
Attribute types — Rank and State types