---
id: operations
title: 'Operations in Agility'
description: 'Understanding operations - the named actions that change asset lifecycle state in Digital.ai Agility'
sidebar_label: 'Operations'
sidebar_position: 4
tags: ['concepts', 'operations', 'state-management']
---

Operations are named actions defined per [asset type](/concepts/assets) 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:

```text
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**:

```bash
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](/apis/asset-bulk/operations) —
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](/troubleshooting); recoverable until purged |
| `Undelete`   | Restores a deleted asset | Back to its prior state                                                                             |

:::warning Inactivate is not Delete
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** — `Delete` on it fails with
  `Violation'AssetCannotDeleteClosed`. `Reactivate` first, then `Delete`.
- **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:

```mermaid
stateDiagram-v2
    [*] --> Active : create
    Active --> Closed : Inactivate
    Closed --> Active : Reactivate
    Active --> Deleted : Delete (soft)
    Deleted --> Active : Undelete
    note right of Closed
        Delete refused (AssetCannotDeleteClosed)
        Writes are backfill-only
    end note
```

`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](/apis/asset-bulk/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:

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

```json
{
  "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](/guides/reproducing-reported-numbers).)

Then set the status like any relation, on either write surface:

```xml
<!-- rest-1.v1, XML -->
<Asset>
  <Relation name="Status" act="set"><Asset idref="StoryStatus:134"/></Relation>
</Asset>
```

```json
// rest-1.v1 also accepts JSON, with an Attributes wrapper — verified live
{ "Attributes": { "Status": { "value": "StoryStatus:134", "act": "set" } } }
```

```json
// 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](/concepts/moments-and-history) and the
[`/rest-1.v1/Hist` namespace](/apis/rest-1).

## Permissions

Whether you may invoke an operation is governed by your member's role in the
asset's scope — see
[Roles and permissions](/reference/roles-and-permissions). A denied
operation fails; handle it like any other authorization error.

## Code Example

```typescript
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](/concepts/assets) — AssetState vs Status
- [Bulk API operations](/apis/asset-bulk/operations) — execute across many assets
- [rest-1 operations](/apis/rest-1/operations) — the `?op=` protocol in detail
- [Workflows and status](/concepts/workflows) — the workflow side of the model
