Runs
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.
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
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.