> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lumos.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Long-running operations

> How v1 reports work that finishes after the response: the operation object, how to poll it, and what each operation type returns.

Some work takes longer than a request should wait. An endpoint that starts this work returns
`202 Accepted` right away, with an operation that describes the work. You poll the operation until
the work finishes. Every operation type has the same shape, so one polling loop handles all of
them.

Four endpoints work this way:

| Endpoint | What it starts | `metadata.type` |
| - | - | - |
| [Trigger App Sync](https://docs.lumos.com/api-reference/v1/apps/trigger-app-sync.md), `POST /v1/apps/{app_uuid}:sync` | A sync of the app | `app_sync` |
| [Upload Accounts](https://docs.lumos.com/api-reference/v1/accounts/upload-accounts.md), `POST /v1/accounts:upload` | Processing the uploaded accounts | `account_upload` |
| [Sync App Review](https://docs.lumos.com/api-reference/v1/access-reviews/sync-app-review.md), `POST /v1/access-reviews/{review_uuid}/apps/{app_review_uuid}:sync` | The sync for one app review | `access_review_app_sync` |
| [Delete App](https://docs.lumos.com/api-reference/v1/apps/delete-app.md), `DELETE /v1/apps/{app_uuid}` | Deleting an app connected through an integration | `app_delete` |

## The operation object

The call that starts the work wraps the operation in an `operation` key:

```json theme={null}
{
  "operation": {
    "id": "YXBwX3N5bmM6MWEyYjNjNGQtNWU2Zi03ODkwLWFiY2QtZWYxMjM0NTY3ODkwOmZ1bGxfc3luYw==",
    "done": false,
    "result": null,
    "error": null,
    "metadata": {
      "type": "app_sync",
      "app_id": "1a2b3c4d-5e6f-7890-abcd-ef1234567890",
      "sync_type": "full-sync"
    }
  }
}
```

| Field | Description |
| - | - |
| `id` | An opaque identifier. Poll with it. Don't decode it or build one. |
| `done` | `false` while the work runs, `true` once it has finished, whether it succeeded or failed. |
| `result` | What the work produced. Set only when `done` is `true` and the work succeeded. Its shape depends on the operation type. |
| `error` | Why the work failed. Set only when `done` is `true` and the work failed. |
| `metadata` | The operation type, in `type`, and what the work runs against. This field is informational. New keys can appear over time. |

While `done` is `false`, neither `result` nor `error` is set. When `done` is `true`, exactly one of
them is set. Check `done` first. Then check which of the two fields is present.

An operation's `error` isn't the [error envelope](https://docs.lumos.com/reference/v1/errors.md) that an HTTP failure
returns. It has a `code` to branch on, a human-readable `message`, and an `errors` list for
item-level failures. No operation type itemizes its failures today, so `errors` is empty.

## Polling

Poll [Get Operation](https://docs.lumos.com/api-reference/v1/operations/get-operation.md) with the operation's `id`. It
returns the operation itself, without the `operation` wrapper:

```bash theme={null}
curl -s 'https://api.lumos.com/v1/operations/YXBwX3N5bmM6MWEyYjNjNGQtNWU2Zi03ODkwLWFiY2QtZWYxMjM0NTY3ODkwOmZ1bGxfc3luYw==' \
  --header 'Authorization: Bearer lsk_your_token'
```

Polls count against your [rate limit](https://docs.lumos.com/reference/v1/authentication.md#rate-limits) like any other
request. Instead of polling in a tight loop, wait between polls and increase the wait each time:

```python theme={null}
import time

def wait_for(operation, max_delay=60):
    # Poll until the work finishes, doubling the wait each time.
    delay = 2
    while not operation["done"]:
        time.sleep(delay)
        delay = min(delay * 2, max_delay)
        operation = get(f"/v1/operations/{operation['id']}")
    if operation.get("error"):
        raise RuntimeError(operation["error"]["code"])
    return operation["result"]


started = post(
    "/v1/apps/1a2b3c4d-5e6f-7890-abcd-ef1234567890:sync",
    json={"sync_type": "full-sync"},
)
result = wait_for(started["operation"])
```

## What each operation type returns

### App sync

Started by [Trigger App Sync](https://docs.lumos.com/api-reference/v1/apps/trigger-app-sync.md). `metadata` carries `type`,
`app_id`, and the `sync_type` that you requested.

* A successful sync sets `result` to `{ "app_id": "…", "sync_status": "success" }`.
* A failed sync sets `error.code` to `app_sync.failed`.

An app sync operation is a view of the app's latest sync, not a record of one run. If another sync
starts after yours, polling the same ID reports that newer sync. The operation reports the same
state as the app's `sync_status` field.

### Account upload

Started by [Upload Accounts](https://docs.lumos.com/api-reference/v1/accounts/upload-accounts.md). `metadata` carries `type`
and `app_id`. Each upload gets its own operation, so the ID tracks that one upload.

* A successful upload sets `result` to `{ "app_id": "…", "state": "SUCCESS" }`.
* A failed upload sets `error.code` to `account_upload.failed`.
* An upload that's cancelled before it finishes sets `error.code` to `account_upload.cancelled`.

### App review sync

Started by [Sync App Review](https://docs.lumos.com/api-reference/v1/access-reviews/sync-app-review.md). `metadata` carries
`type` and `app_review_id`.

* A successful sync sets `result` to `{ "app_review_id": "…", "status": "…" }`, where `status` is
  the app review's current status, such as `assigning-reviewers`.
* A failed sync sets `error.code` to `access_review_app_sync.failed`.

Like an app sync operation, this operation tracks the app review's latest sync, not one run.

### App delete

Started by [Delete App](https://docs.lumos.com/api-reference/v1/apps/delete-app.md) for an app connected through an
integration. `metadata` carries `type` and `app_id`.

* A successful delete sets `result` to `{ "app_id": "…" }`, and the app no longer exists.
* `error.code` `app_delete.record_retained` means other Lumos data still references the app, so it
  stays, disconnected.
* `error.code` `app_delete.failed` means removing the app's accounts failed. The app is already
  disconnected, so retry the delete.

## Who can poll

You can poll an operation if you're allowed to start that type of work on its resource, or if you
can read that resource. Anyone else in your organization gets `403`.

## Errors when polling

An unknown or malformed operation ID returns `404`, as does an operation whose resource has since
been deleted. An app delete operation is the exception: you can still poll it after the app is
gone.
