Retry a start safely
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
Section titled “Before you start”- 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_URLandKINDGI_API_TOKENset.
Pick a key that names the work
Section titled “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
Section titled “Start with the key”Starting the same run twice shows what a repeat gets.
Run this twice:
pnpm exec kindgi runs start --flow=acme.review-order --input='{"orderId":"A-100"}' --idempotency-key=order-A-100-reviewuv run kindgi runs start --flow=acme.review-order --input='{"orderId":"A-100"}' --idempotency-key=order-A-100-review./kindgiw runs start --flow=acme.review-order --input='{"orderId":"A-100"}' --idempotency-key=order-A-100-review./kindgiw runs start --flow=acme.review-order --input='{"orderId":"A-100"}' --idempotency-key=order-A-100-review--flow=acme.review-orderis the id of the flow to start.--input='{"orderId":"A-100"}'is the run's input, as JSON.--idempotency-key=order-A-100-reviewis the key that names this start.
Both print the same run, with the same id and createdAt.
Send the key from your app
Section titled “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:
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 answeredconsole.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 completedconflict Idempotency-Key was reused with a different request body. Use a fresh key or the original body.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 answeredprint(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:
True completedIdempotency-Key was reused with a different request body. Use a fresh key or the original body.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(); }}true completedconflict 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.
package acme.app
import com.kindgi.client.{ConflictException, Kindgi}import com.kindgi.client.models.StartRunBodyimport com.kindgi.client.resources.RunsStartParamsimport 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}") } }}true completedconflict 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
Section titled “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
getfor 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, andRetry-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.