Skip to content

Follow a run from the browser

View as Markdown

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.

  • 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 dev running in your pack's directory.

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:

Terminal window
echo 'KINDGI_CORS_ORIGINS=http://localhost:5173' >> .env

http://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.

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:

app/reservations.ts
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;
}

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.

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:

web/progress.ts
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.
  • apiUrl is your Kindgi API's URL: kindgi dev's here.
  • accessToken is the token your backend handed the page.
  • refreshAccessToken asks your backend for a fresh token.

In a page served from http://localhost:5173, for a run of acme.reserve-order, showProgress gets:

run.started
run.step-started reserve
run.step-retry-scheduled reserve
run.step-retry-scheduled reserve
run.step-completed reserve
run.completed

subscribeToRun reconnects by itself when the server ends the stream. It calls refreshAccessToken when the token expires.

  • Only follow progress. The token works on two routes, GET /v1/runs/{runId}/progress and GET /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:2
    event: run.step-started
    data: {"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 dev makes when it starts, so they stop working when it restarts.