Follow a run from the browser
This guide lets a web page show a run's progress straight from Kindgi, with a short-lived, read-only token instead of your API token.
Your backend starts the run and hands the page two things: the run's id and a public run token. The page follows the run's progress with them, straight from Kindgi. The token can do nothing else, and it expires.
Before you start
Section titled “Before you start”- A flow to start, registered on your runtime. This guide starts
acme.reserve-order; use your own flow's id. - A backend that starts runs with Kindgi 0.1's client, on Node 22.12 or later, Python 3.11 or later, or Java 17 or later, as Start a run shows.
- A web page that imports npm packages, such as one a bundler like Vite
builds, for
@kindgi/sdk/client. kindgi devrunning in your pack's directory.
1. Allow your site's origin
Section titled “1. Allow your site's origin”The browser calls Kindgi from your site's origin, so Kindgi must allow it.
List the origins in KINDGI_CORS_ORIGINS, comma-separated. In development,
add it to the pack's .env:
echo 'KINDGI_CORS_ORIGINS=http://localhost:5173' >> .envhttp://localhost:5173 is your page's origin: its scheme, host and port.
Restart kindgi dev. Its banner shows the origin:
Origins http://localhost:5173 (browsers may follow runs with public run tokens)In a deployment, set it in the runtime's environment, as
KINDGI_CORS_ORIGINS
describes.
2. Hand the page a token
Section titled “2. Hand the page a token”The page needs a token for the run it shows, and a fresh one when that token expires.
When the deployment issues public run tokens, every start returns
publicAccessToken: a token for the run it started, valid for 15 minutes by
default. kindgi dev issues them. A deployment issues them when it has a
signing key,
KINDGI_PUBLIC_TOKEN_SIGNING_KEY_PATH.
In your backend, pass the token on with the run's id when a run starts, and mint a fresh one when the page asks. Ask your coding agent: "When my backend starts a run, I want it to give my web page a token to follow it, and a fresh one when that expires." For reference, the code looks like this:
import { createClient } from '@kindgi/sdk/client';import type { RunId } from '@kindgi/sdk/types';
const kindgi = createClient(); // KINDGI_API_URL, KINDGI_API_TOKEN
/** When the user starts a reservation. */export async function startReservation(orderId: string) { const run = await kindgi.runs.start({ flow: 'acme.reserve-order', input: { orderId }, options: { wait: false }, }); return { runId: run.id, token: run.publicAccessToken };}
/** When the page asks for a fresh token. Check first that the user may see this run. */export async function freshToken(runId: RunId) { const { token } = await kindgi.tokens.createPublic({ runIds: [runId] }); return token;}from kindgi.client import Kindgi
kindgi = Kindgi() # KINDGI_API_URL, KINDGI_API_TOKEN
def start_reservation(order_id: str) -> dict[str, str | None]: """When the user starts a reservation.""" run = kindgi.runs.start( flow="acme.reserve-order", input={"orderId": order_id}, options={"wait": False} ) return {"runId": str(run.id), "token": run.public_access_token}
def fresh_token(run_id: str) -> str: """When the page asks for a fresh token. Check first that the user may see this run.""" return kindgi.tokens.mint_public(run_ids=[run_id]).tokenpackage com.acme.app;
import com.kindgi.client.Kindgi;import com.kindgi.client.models.MintPublicRunTokenBody;import com.kindgi.client.models.Run;import com.kindgi.client.models.StartRunBody;import java.util.List;import java.util.Map;import java.util.UUID;
public final class RunTokens { private static final Kindgi kindgi = Kindgi.create(); // KINDGI_API_URL, KINDGI_API_TOKEN
public record Started(UUID runId, String token) {}
/** When the user starts a reservation. */ public static Started startReservation(String orderId) { Run run = kindgi.runs().start(StartRunBody.WithFlow.builder() .flow("acme.reserve-order") .input(Map.of("orderId", orderId)) .options(StartRunBody.WithFlow.Options.builder().wait_(false).build()) .build()); return new Started(run.id(), run.publicAccessToken()); }
/** When the page asks for a fresh token (check first that the user may see this run). */ public static String freshToken(UUID runId) { return kindgi.tokens().mintPublic(MintPublicRunTokenBody.builder() .runIds(List.of(runId.toString())) .build()).token(); }
private RunTokens() {}}package acme.app
import com.kindgi.client.Kindgiimport com.kindgi.client.models.{MintPublicRunTokenBody, StartRunBody}import java.util.UUIDimport scala.jdk.CollectionConverters._
object RunTokens { private val kindgi = Kindgi.create() // KINDGI_API_URL, KINDGI_API_TOKEN
final case class Started(runId: UUID, token: String)
/** When the user starts a reservation. */ def startReservation(orderId: String): Started = { val run = kindgi.runs().start(StartRunBody.WithFlow.builder() .flow("acme.reserve-order") .input(Map("orderId" -> orderId).asJava) .options(StartRunBody.WithFlow.Options.builder().wait_(false).build()) .build()) Started(run.id, run.publicAccessToken) }
/** When the page asks for a fresh token (check first that the user may see this run). */ def freshToken(runId: UUID): String = kindgi.tokens().mintPublic(MintPublicRunTokenBody.builder() .runIds(List(runId.toString).asJava) .build()).token}Serve each from a route of your backend. This guide calls them
POST /api/reservations and GET /api/run-token?runId=<run-id>.
The page now gets a run's id and its token, and can ask for a fresh token. A
minted token can name up to 50 runs. It lives from a second to a day, as
expiresInSeconds sets, or expires_in_seconds in Python.
3. Follow the run in the page
Section titled “3. Follow the run in the page”The page shows each step as the run takes it.
Ask your coding agent: "I want my web page to show the run's progress as it
happens." For reference, the page's code looks like this. It uses
subscribeToRun from @kindgi/sdk/client, which runs in the browser:
import { subscribeToRun } from '@kindgi/sdk/client';
const progress = document.querySelector('#progress');
/** The page's own code: one line per event. */function showProgress(kind: string, nodeId?: string) { progress?.append(nodeId === undefined ? `${kind}\n` : `${kind} ${nodeId}\n`);}
const { runId, token } = await fetch('/api/reservations', { method: 'POST' }).then((r) => r.json());
for await (const event of subscribeToRun({ apiUrl: 'http://127.0.0.1:4313', // your Kindgi API runId, accessToken: token, refreshAccessToken: () => fetch(`/api/run-token?runId=${runId}`).then((r) => r.text()),})) { showProgress(event.kind, event.nodeId); // run.step-started reserve, …}// The loop ends after run.completed, run.failed or run.cancelled.apiUrlis your Kindgi API's URL:kindgi dev's here.accessTokenis the token your backend handed the page.refreshAccessTokenasks your backend for a fresh token.
In a page served from http://localhost:5173, for a run of
acme.reserve-order, showProgress gets:
run.startedrun.step-started reserverun.step-retry-scheduled reserverun.step-retry-scheduled reserverun.step-completed reserverun.completedsubscribeToRun reconnects by itself when the server ends the stream. It
calls refreshAccessToken when the token expires.
What the token can do
Section titled “What the token can do”-
Only follow progress. The token works on two routes,
GET /v1/runs/{runId}/progressandGET /v1/runs/{runId}/progress/stream. It works for the runs it names and the runs inside them, such as an agent step's turn. Any other call is refused:{"error":{"code":"permission-denied","message":"A public run token can only follow the runs it names: GET /v1/runs/{runId}/progress and GET /v1/runs/{runId}/progress/stream","requestId":"req-19ffb614-4c27-438b-95dd-5ace0de7125b"}} -
No data. Progress events have the kind, the step and the time, but no inputs, outputs or errors:
id: f7bb2e8c-6895-4ae4-a377-065be3a6a952:2event: run.step-starteddata: {"eventId":"f7bb2e8c-6895-4ae4-a377-065be3a6a952:2","runId":"f7bb2e8c-6895-4ae4-a377-065be3a6a952","timestamp":"2026-10-03T20:13:13.452Z","kind":"run.step-started","sequence":2,"nodeId":"reserve"}Show the result from your backend, which reads the run with your API token.
-
No revoking one by one. Keep lifetimes short. In development, tokens are signed with a key
kindgi devmakes when it starts, so they stop working when it restarts.