Skip to content
emitDocs

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.

  1. Decide who may follow

    Each space has a Who may follow a job setting (publicSubscriptions in the API):

    Setting A browser needs
    signed (the default) a follow token from your server, for that job
    unsigned only the job's id
    disabled nothing works: only API tokens can follow

    Keep signed unless job ids alone are safe to hand out.

  2. 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 read scope.

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

  3. Follow the job in the browser

    With the TypeScript SDK, make a client with token: null and pass the follow token to follow. Without an SDK, the same stream is a plain EventSource.

    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, retrying when an attempt failed and another is coming, lines for new notes, and finished last. 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.