# Runs

> For the complete documentation index, see [llms.txt](https://docs.lertha.com/llms.txt).
> Any documentation page is available as raw Markdown by appending `.md` to its URL.

> A run is one execution of a task or flow. Its id is chosen by you and doubles as the idempotency key.

A **run** is one execution of a task or a flow. You choose its id, and the id is the
idempotency key: the same id twice is the same work.

```kotlin
val options = RunOptions(
    id = "otp-${otpRow.id}",           // stable, unique per unit of work
    queue = "lab",
    attemptTimeout = 10.seconds,
    overallTimeout = 2.minutes,
    retry = RetryPolicy(maxAttempts = 5, initialInterval = 500.milliseconds),
)
val handle = orchestrator.start(SendOtp.type, input, options)
```

## States

```mermaid
stateDiagram-v2
  [*] --> Scheduled
  Scheduled --> Running
  Running --> Scheduled: attempt failed, retry due
  Running --> Completed
  Running --> Failed
  Running --> TimedOut
  Running --> Cancelled
  Running --> Paused
  Paused --> Running
  Scheduled --> Terminated
  Running --> Terminated
```

`handle.status()` returns the state and the attempt number. `handle.result()` suspends until a
terminal state and throws `RunFailed` for anything but `Completed`; the exception carries the
state and the handler's own message.

## Id policies

What happens when a run with the same id already exists depends on whether it is still running.

**Still running** (`ConflictPolicy`):

| Policy | Effect |
| --- | --- |
| `FAIL` (default) | the second start throws `RunAlreadyExists` |
| `USE_EXISTING` | the second caller gets a handle to the run in progress; server-side de-duplication |

**Already finished** (`ReusePolicy`):

| Policy | Effect |
| --- | --- |
| `ALLOW_DUPLICATE_FAILED_ONLY` (default) | never redo a success, always allow another go at a failure |
| `ALLOW_DUPLICATE` | a new run under the same id |
| `REJECT_DUPLICATE` | never again, whatever happened |

An OTP send wants `REJECT_DUPLICATE`: a failed send must not be re-run under an old id after the
code has expired. A nightly recompute wants the default.

## Re-attaching

A handle is not the run. `orchestrator.handle(type, RunId("..."))` re-attaches from any process,
and `result()` on it returns the recorded result, however long ago it finished. The result lives
in the orchestrator, not in the client that started it.

**Remember:** choose the id like a database key, because that is what it is.
