Live View

View as Markdown

The live view gives you an embeddable iframe for the active run. Use it to watch automation as it happens, or hand control to a human for an in-the-loop step.

Get the run from the runtime, then create the viewer from the run.

1const run = await runtime.currentRun();
2
3const { iframeUrl } = await run.live({
4 control: "none",
5});

Drop iframeUrl into an iframe. The URL is opaque and short-lived.

1<iframe src={iframeUrl} style="width: 100%; height: 600px; border: none;" />
Viewer URLs are opaque

The URL is backed by a short-lived server-side lease. Do not parse it, build sub-URLs from it, or store it long-term. When expiresAt passes, create a new viewer URL.

Options

1const { iframeUrl, activityStreamUrl, expiresAt } = await run.live({
2 control: "input",
3 components: ["activity"],
4});
ParameterTypeRequiredDescription
control"none" | "input"No"none" is watch-only. "input" forwards mouse and keyboard input to the browser.
componentsArray<"activity">NoRequest viewer add-ons. Pass ["activity"] to receive activityStreamUrl.
expiresInSecondsnumberNoLease lifetime override. Default is one hour; max is 24 hours.

Watch-only vs interactive

1const monitor = await run.live();
2
3const handoff = await run.live({
4 control: "input",
5});

control: "none" is enforced server-side. Use it for dashboards and shared viewers. Use control: "input" only when a user should be able to control the browser.

Refresh an expired viewer

1async function refreshViewer(runtime, iframe) {
2 const run = await runtime.currentRun();
3 const viewer = await run.live({ control: "input" });
4
5 iframe.src = viewer.iframeUrl;
6
7 const expiresIn = new Date(viewer.expiresAt).getTime() - Date.now();
8 setTimeout(() => refreshViewer(runtime, iframe), Math.max(60_000, expiresIn - 60_000));
9}

HTTP equivalent

1POST /v1/runs/{runId}/live