Guides
Show progress in a browser
Follow a job live from your frontend, with no API token in the browser and no polling.
A browser can follow a job as it runs: its status, its progress and its notes, pushed the moment they change. It needs no API token. Your server asks emit for a follow token for one job, and the browser follows that job with it.
A browser following a job sees its status, progress and notes. It never sees the payload, the result, the error, events or log lines.
Decide who may follow
Each space has a Who may follow a job setting (
publicSubscriptionsin the API):Setting A browser needs signed(the default)a follow token from your server, for that job unsignedonly the job's id disablednothing works: only API tokens can follow Keep
signedunless job ids alone are safe to hand out.Get a follow token on your server
Create the job, then ask for a token that lets its holder follow it, for a second up to a year. This needs an API token with the
readscope.// POST /exports, in your API const { id } = await emit.jobs.create("export-csv", { accountId: "acc_19", month: "2026-09" }); const followToken = await emit.jobs.followToken(id, { expiresIn: "1h" }); return Response.json({ jobId: id, followToken });created, err := c.Jobs.Create(ctx, emit.NewJob{Name: "export-csv", Payload: map[string]string{"accountId": "acc_19", "month": "2026-09"}}) if err != nil { return err } followToken, err := c.Jobs.FollowToken(ctx, created.ID, time.Hour) if err != nil { return err } json.NewEncoder(w).Encode(map[string]string{"jobId": created.ID, "followToken": followToken})curl -s -X POST "$EMIT_URL/v1/spaces/exports/jobs/01M3QSGN0103P7EGVBX9D5NTSN:followToken" \ -H "Authorization: Bearer $EMIT_TOKEN" \ -H "Content-Type: application/json" \ -d '{"expiresInMs": 3600000}' # {"token":"t=1790729747943,v1=09360a4d2d6c..."}emit signs the token itself, so there's no secret for you to keep. A token only works for its job, on the cluster that holds it.
Follow the job in the browser
With the TypeScript SDK, make a client with
token: nulland pass the follow token tofollow. Without an SDK, the same stream is a plainEventSource.import { Emit } from "@emit.run/sdk"; const emit = new Emit({ url: "https://<your cluster's endpoint>", token: null, space: "exports" }); const { jobId, followToken } = await fetch("/exports", { method: "POST" }).then((r) => r.json()); for await (const event of emit.jobs.follow(jobId, { token: followToken })) { if (event.progress) progressBar.value = event.progress.percent; if (event.finished) statusLine.textContent = event.finished.status; // "completed", "failed" or "cancelled" }const { jobId, followToken } = await fetch("/exports", { method: "POST" }).then((r) => r.json()); const url = `https://<your cluster's endpoint>/v1/spaces/exports/jobs/${jobId}/events?token=${encodeURIComponent(followToken)}`; const events = new EventSource(url); events.onmessage = (message) => { const event = JSON.parse(message.data); if (event.progress) progressBar.value = event.progress.percent; if (event.finished) { statusLine.textContent = event.finished.status; // "COMPLETED", "FAILED" or "CANCELLED" events.close(); // otherwise the browser reconnects } };The first message is the job as it is now (
now). After it comes one message per change:progress,retryingwhen an attempt failed and another is coming,linesfor new notes, andfinishedlast. The SDK gives statuses in lower case and times as numbers. The raw stream gives what the HTTP API sends: statuses in upper case, and times in milliseconds as strings.
Following a workflow run
For a workflow run, ask for the whole run as a tree: emit.jobs.follow(id, { token, tree: true }), or &tree=true on the stream's URL. Each message is then the run with its steps, sleeps and signals, sent again whenever any of them changes. Write wf.note("Payment confirmed") in the workflow, or job.note(...) in a step, to put a line on the run's timeline for people to read.