Operations in Agility
Operations are named actions defined per asset type that change an asset's lifecycle state or trigger built-in behaviour — closing, deleting, copying, converting. They are distinct from attribute updates: an operation is invoked, not set.
Discovering Operations
Each type's operations are listed in its Meta API description. Story on a
26.2 instance:
Inactivate, Reactivate, Delete, Undelete, Copy, ShallowCopy, DeepCopy,
QuickClose, QuickSignup, Split, Breakdown, ConvertToDefect, MakeTemplate
The set varies by type — check GET /meta.v1/<AssetType> for yours. Naming
varies too: most types close via Inactivate, while Timebox has Close.
Invoking an Operation
On rest-1.v1, POST to the asset with ?op= and an empty body:
curl -X POST -H "Authorization: Bearer ${AGILITY_ACCESS_TOKEN}" \
"${AGILITY_BASE_URL}/rest-1.v1/Data/Story/3884?op=Inactivate"
The response is the asset, including its OID with the new moment
(Story:3884:6277).
On api/asset, use an execute command —
which can run one operation across many assets in a single request.
The Lifecycle Quartet
The four operations nearly every type has, and what they actually do:
| Operation | Effect | After it runs |
|---|---|---|
Inactivate | Closes the asset | AssetState: Closed, IsDeleted: False |
Reactivate | Reopens a closed asset | AssetState: Active |
Delete | Soft-deletes the asset | Hidden from queries unless you ask for deleted assets; recoverable until purged |
Undelete | Restores a deleted asset | Back to its prior state |
A persistent confusion, worth stating plainly: Inactivate closes; it does
not touch IsDeleted. Closed assets remain fully queryable. Delete is
the soft delete — and even deleted assets are still stored and recoverable
until an administrator purges them.
Two interactions between the pairs:
- A Closed asset cannot be deleted —
Deleteon it fails withViolation'AssetCannotDeleteClosed.Reactivatefirst, thenDelete. - A Closed asset accepts only backfill — writing an attribute that had
no value succeeds without reopening (by design, so missing values can be
pushed after close), but changing an existing value fails with
Violation'ClosedAssetCannotEditExistingValues.
The whole lifecycle in one picture:
Undelete has a quirk when invoked through the Bulk API: the update/execute
command must include IsDeleted: true to find its target — see
updating assets.
The Copy Family
Copy, ShallowCopy and DeepCopy create new assets from an existing one.
On rest-1.v1 the response hands you the new asset:
POST /rest-1.v1/Data/Story/3884?op=Copy
→ <Asset id="Story:3885:6280" …> ← the copy's OID
If your integration needs the copy's OID, invoke the operation through
rest-1.v1 — the Bulk API's execute command does not return the
OID of an asset an operation generates.
The Convert Family
ConvertToDefect (on Story) and ConvertToStory (on Defect) migrate a
work item across types. The operation creates a new asset of the
destination type: attribute values the two types share are copied, child
tasks and tests move, effort records move, and attachments and links are
copied. The source asset is then renamed <Name> (Converted To D-01234)
and soft-deleted. The conversion is transactional — if any step fails,
nothing changes.
Required attributes are enforced when the new asset is saved. An attribute
that is required on the destination type but ends up with no value —
typically one that exists only on the destination type, such as a required
custom field — fails the whole conversion with
Violation'Required'AttributeDefinition'<name>'<Type>, and the source work
item is left untouched.
The conversion operation itself accepts argument values for destination
attributes — that is how the product UI satisfies destination-only required
fields when it converts. API delivery of those arguments is limited: the
body of a rest-1.v1 ?op= POST is ignored, and through the Bulk API's
execute command args map, only text-type argument values (Text,
LongText) reach the conversion — keys are full attribute tokens on the
destination type, e.g. "Defect.FoundBy". Numeric, relation, and
multivalue argument values fail, and the conversion fails with them. For
a required destination field that isn't text: populate it on the source
first if the types share it, and if it exists only on the destination
type, complete the conversion in the UI.
As with the copy family, the Bulk API's execute command does not return the
new asset's OID — rest-1.v1 does. To locate the asset a Bulk API
conversion created, query the destination type: attribute values, including
Name, are copied to it.
Status Changes Are Not Operations
Moving a work item through its workflow — Future → In Progress → Done
— is an ordinary attribute update to Status, which is a relation to a
list value. Discover the values by querying the list type:
{
"from": "StoryStatus",
"select": ["Name", "Order", "RollupState"],
"sort": ["Order"],
"page": { "size": 50, "start": 0 }
}
(Status list values carry Name, Order, Description and RollupState —
there is no IsClosed flag. RollupState is what reporting uses to treat a
status as effectively closed; see
reproducing reported numbers.)
Then set the status like any relation, on either write surface:
<!-- rest-1.v1, XML -->
<Asset>
<Relation name="Status" act="set"><Asset idref="StoryStatus:134"/></Relation>
</Asset>
// rest-1.v1 also accepts JSON, with an Attributes wrapper — verified live
{ "Attributes": { "Status": { "value": "StoryStatus:134", "act": "set" } } }
// api/asset update command
{ "from": "Story:3884", "update": { "Status": "StoryStatus:134" } }
The API does not walk a transition graph: Status can be set directly to
any active list value. Board-style transition rules are a UI concern. Use
OIDs for the values — status names are localized and instance-editable.
History of Operations
Every operation writes a new moment. To see who did what and when, read the
asset's history — see
Moments and history and the
/rest-1.v1/Hist namespace.
Permissions
Whether you may invoke an operation is governed by your member's role in the asset's scope — see Roles and permissions. A denied operation fails; handle it like any other authorization error.
Code Example
class OperationClient {
constructor(
private baseUrl: string,
private headers: HeadersInit
) {}
/** Invoke a lifecycle operation. Returns the response asset's OID. */
async execute(oid: string, operation: string): Promise<string> {
const [type, id] = oid.split(':');
const response = await fetch(
`${this.baseUrl}/rest-1.v1/Data/${type}/${id}?op=${operation}`,
{ method: 'POST', headers: this.headers }
);
if (!response.ok)
throw new Error(`${operation} failed: ${response.status}`);
const text = await response.text();
return /id="([^"]+)"/.exec(text)?.[1] ?? '';
}
}
// Close a story; copy one and get the copy's OID
const client = new OperationClient(baseUrl, headers);
await client.execute('Story:3884', 'Inactivate');
const copyOid = await client.execute('Story:3884', 'Copy');
Related Topics
- Assets — AssetState vs Status
- Bulk API operations — execute across many assets
- rest-1 operations — the
?op=protocol in detail - Workflows and status — the workflow side of the model