Skip to content

Retry a start safely

View as Markdown

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.

  • A registered flow to start. This guide starts acme.review-order; use your own flow's id. Write a flow 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.

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.

Starting the same run twice shows what a repeat gets.

Run this twice:

Terminal window
pnpm exec kindgi 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.

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:

app/start-review.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:

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

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

  • 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:

    {"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.

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