Skip to main content

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 typeStatus list type
Story, Defect, TestSetStoryStatus — one shared list for all primary workitems
TaskTaskStatus
TestTestStatus
EpicEpicStatus

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:

AttributeTypeNotes
NameTextInstance data — editable, so code should match on OID, not name
OrderRankOpaque ordering token: sort by it, never read it
DescriptionLongText
RollupStateStateThe 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 nameRollupState
FutureFuture
In ProgressActive
TestingActive
In UATClosed
DoneClosed
AcceptedClosed

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:

  1. 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 StoryStatus values 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 with Violation'PrimaryWorkitemCannotHaveInvalidStatus. (Status values carry a Team relation — a team-scoped value belongs to that team's process.)

    The process itself is queryable, though the attribute names are not guessable: Team.HasProcess says whether the team enforces one, and Team.SelectedValues is the allowed set.

    { "from": "Team", "select": ["Name", "HasProcess", { "from": "SelectedValues", "select": ["Name"] }] }

    A team with HasProcess: "False" has an empty SelectedValues and enforces nothing. A status value belongs to a team's process or to a project scheme, never both — creating one with both a Team and SelectedInSchemes fails with Violation'ListValueCannotBelongToBothSchemeAndTeam. To ask the same question for one specific workitem — with every narrowing already applied — use api/ValidValues instead.

  2. Use OIDs. Status names are instance data and freely editable.

  3. Status is not lifecycle. Setting a Closed-rolling status does not close the asset; AssetState changes only through operations like Inactivate. An asset can be Status = Done and AssetState = 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 SelectedInSchemes relation).
  • Team Process — a team can have its own set of StoryStatus values 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 whose RollupState is 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 with Scheme.IsRequiredAtClose they become mandatory when the asset is closed — so a close can fail validation even though ordinary updates never did. The api/canbeimportant endpoint 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 } }