# Retry a start safely

> Send an idempotency key with a start, so a retried request returns the run the first one started instead of starting another.

This guide makes a retried start return the run the first request started, instead of starting a second run.

A request to start a run can fail on its way back, after the run started, such as when the connection drops. A start sent with an **idempotency key** is safe to repeat: once the first request has answered, a repeat with the same key returns the run the first one started.

## Before you start

- **A registered flow** to start. This guide starts `acme.review-order`; use your own flow's id. [Write a flow](https://docs.kindgi.com/v0.1.10/guides/flows/write-a-flow.md) shows how to write and register one.
- **A runtime to start it on,** such as `kindgi dev`, and Kindgi 0.1's CLI.
- **For the app's half:** Node 22.12 or later, Python 3.11 or later, or Java 17 or later, with Kindgi 0.1's client, and `KINDGI_API_URL` and `KINDGI_API_TOKEN` set.

## Pick a key that names the work

The key tells Kindgi which starts are the same work.

Name the work, not the attempt: `review-A-200` for "review order A-200". Use the same key on every retry of that start.

Each retry now carries the key of the first request. A new key per attempt would start a new run each time.

## Start with the key

Starting the same run twice shows what a repeat gets.

Run this twice:

- **TypeScript**

  ```sh
  pnpm exec kindgi runs start --flow=acme.review-order --input='{"orderId":"A-100"}' --idempotency-key=order-A-100-review
  ```

- **Python**

  ```sh
  uv run kindgi runs start --flow=acme.review-order --input='{"orderId":"A-100"}' --idempotency-key=order-A-100-review
  ```

- **Java**

  ```sh
  ./kindgiw runs start --flow=acme.review-order --input='{"orderId":"A-100"}' --idempotency-key=order-A-100-review
  ```

- **Scala**

  ```sh
  ./kindgiw runs start --flow=acme.review-order --input='{"orderId":"A-100"}' --idempotency-key=order-A-100-review
  ```

* `--flow=acme.review-order` is the id of the flow to start.
* `--input='{"orderId":"A-100"}'` is the run's input, as JSON.
* `--idempotency-key=order-A-100-review` is the key that names this start.

Both print the same run, with the same `id` and `createdAt`.

## Send the key from your app

Your app sends the key with each start, so its own retries are safe.

Pass `idempotencyKey` to `runs.start`, or `idempotency_key` in Python. Ask your coding agent: "I want my app to retry starting a run without starting it twice." For reference, this code starts the same run twice, then sends the same key with a different input:

- **TypeScript**

  `app/start-review.ts`

  ```ts
  import { KindgiApiError, createClient } from '@kindgi/sdk/client';

  const kindgi = createClient(); // KINDGI_API_URL, KINDGI_API_TOKEN

  const start = () =>
    kindgi.runs.start({
      flow: 'acme.review-order',
      input: { orderId: 'A-200' },
      idempotencyKey: 'review-A-200',
    });

  const first = await start();
  const retry = await start(); // the same start again, after the first answered
  console.log(first.id === retry.id, retry.status);

  try {
    await kindgi.runs.start({
      flow: 'acme.review-order',
      input: { orderId: 'A-100' }, // a different request, same key
      idempotencyKey: 'review-A-200',
    });
  } catch (err) {
    if (err instanceof KindgiApiError) console.log(err.error.code, err.message);
  }
  ```

  It prints:

  ```text
  true completed
  conflict Idempotency-Key was reused with a different request body. Use a fresh key or the original body.
  ```

- **Python**

  `app/start_review.py`

  ```python
  from kindgi.client import ConflictError, Kindgi

  kindgi = Kindgi()  # KINDGI_API_URL, KINDGI_API_TOKEN


  def start():
      return kindgi.runs.start(
          flow="acme.review-order", input={"orderId": "A-200"}, idempotency_key="review-A-200"
      )


  first = start()
  retry = start()  # the same start again, after the first answered
  print(first.id == retry.id, retry.status)

  try:
      kindgi.runs.start(
          flow="acme.review-order",
          input={"orderId": "A-100"},  # a different request, same key
          idempotency_key="review-A-200",
      )
  except ConflictError as err:
      print(err)
  ```

  It prints:

  ```text
  True completed
  Idempotency-Key was reused with a different request body. Use a fresh key or the original body.
  ```

- **Java**

  `src/main/java/com/acme/app/RetryStart.java`

  ```java
  package com.acme.app;

  import com.kindgi.client.ConflictException;
  import com.kindgi.client.Kindgi;
  import com.kindgi.client.models.Run;
  import com.kindgi.client.models.StartRunBody;
  import com.kindgi.client.resources.RunsStartParams;
  import java.util.Map;

  public final class RetryStart {
    public static void main(String[] args) {
      Kindgi kindgi = Kindgi.create(); // KINDGI_API_URL, KINDGI_API_TOKEN
      RunsStartParams key = RunsStartParams.builder().idempotencyKey("review-A-200").build();

      Run first = kindgi.runs().start(reviewOrder("A-200"), key);
      Run retry = kindgi.runs().start(reviewOrder("A-200"), key); // the same start again, after the first answered
      System.out.println(first.id().equals(retry.id()) + " " + retry.status());

      try {
        kindgi.runs().start(reviewOrder("A-100"), key); // a different request, same key
      } catch (ConflictException e) {
        System.out.println(e.code() + " " + e.getMessage());
      }
    }

    static StartRunBody reviewOrder(String orderId) {
      return StartRunBody.WithFlow.builder()
          .flow("acme.review-order")
          .input(Map.of("orderId", orderId))
          .build();
    }
  }
  ```

  ```text
  true completed
  conflict Idempotency-Key was reused with a different request body. Use a fresh key or the original body.
  ```

  `RunsStartParams` carries the key. Without one, the client adds a key of its own, so a retry it sends after a dropped connection is safe too.

- **Scala**

  `src/main/scala/acme/app/RetryStart.scala`

  ```scala
  package acme.app

  import com.kindgi.client.{ConflictException, Kindgi}
  import com.kindgi.client.models.StartRunBody
  import com.kindgi.client.resources.RunsStartParams
  import scala.jdk.CollectionConverters._

  object RetryStart {
    def main(args: Array[String]): Unit = {
      val kindgi = Kindgi.create() // KINDGI_API_URL, KINDGI_API_TOKEN
      val key = RunsStartParams.builder().idempotencyKey("review-A-200").build()
      def reviewOrder(orderId: String) =
        StartRunBody.WithFlow.builder().flow("acme.review-order").input(Map("orderId" -> orderId).asJava).build()

      val first = kindgi.runs().start(reviewOrder("A-200"), key)
      val retry = kindgi.runs().start(reviewOrder("A-200"), key) // the same start again, after the first answered
      println(s"${first.id == retry.id} ${retry.status}")

      try kindgi.runs().start(reviewOrder("A-100"), key) // a different request, same key
      catch { case e: ConflictException => println(s"${e.code} ${e.getMessage}") }
    }
  }
  ```

  ```text
  true completed
  conflict Idempotency-Key was reused with a different request body. Use a fresh key or the original body.
  ```

  `RunsStartParams` carries the key. Without one, the client adds a key of its own, so a retry it sends after a dropped connection is safe too.

The retry returned the first run. The same key with a different input was refused, and nothing started.

## How it works

- **A request with a key Kindgi has seen gets the first request's response again:** the same run, as it was returned then. Call `get` for its current state.

- **A repeat sent while the first request is still running doesn't run again.** It's refused with a conflict, `idempotency-key-in-flight`, and `Retry-After: 5`:

  ```json
  {"code":"idempotency-key-in-flight","message":"A request with this Idempotency-Key is still running. Retry after it answers (Retry-After), and you'll get its answer."}
  ```

  Retry after the first one answers, and you get its answer: the same run. The clients don't retry it on their own. A request that stops without answering, such as when its runtime crashed, frees its key within 30 seconds.

- **The key must come with the same request.** The same key with a different body, such as another input or other options, is refused with a conflict. Nothing starts, also while the first request is still running. Bodies are compared byte for byte, as they were sent.

- **Only an answer that succeeded is kept.** A refused start isn't kept, such as one for a flow that isn't registered (`404 flow-not-found`). Once you've fixed the cause, the same start with the same key runs.

- **Keys are kept for 24 hours, per tenant and per caller.** The same key from another person or another API key is their own request, and never gets your response.

- **A response that carries a secret isn't kept.** That's a new API key, a session token, a console session or a generated webhook signing secret. A retry with its key is refused with a conflict, `idempotency-key-replay-withheld`, saying when the first one succeeded. It never gets the secret again, and nothing is made twice.

A waited start answers only when its run completes, fails or waits for something. Until then, a repeat gets `idempotency-key-in-flight`. So start a run that can take long in the background, with `wait: false`: its answer carries the run's id at once. Then [follow it](https://docs.kindgi.com/v0.1.10/guides/runs/follow-a-run.md).

Other requests that change something take an idempotency key too, such as `cancel` and registering a webhook endpoint.

## Next

- [Follow a run](https://docs.kindgi.com/v0.1.10/guides/runs/follow-a-run.md)
- [Cancel a run](https://docs.kindgi.com/v0.1.10/guides/runs/cancel-a-run.md)
- [Get a webhook when a run finishes](https://docs.kindgi.com/v0.1.10/guides/webhooks/receive-run-finished.md)
