AbsoluteJS

Demo — Browser Runtime

@absolutejs/demo is the orchestration layer for enterprise-grade AI demos: drive the product, narrate with AI voiceover, record the screen, and draw presenter-style highlights over the UI. It is intentionally adapter-first — Playwright is excellent for web apps, but real demos often need Discord, Google Meet, native apps, screen switching, and OS-level focus control, so every capability sits behind a stable driver interface.

#Quick Start

Install Bun and FFmpeg (the ffmpeg binary must be on PATH), then run bun add @absolutejs/demo @absolutejs/sync, bun add -d playwright, and bunx playwright install chromium. The root export loads the sync-plan helper, so install sync too. Set ELEVENLABS_API_KEY in your environment or local .env file. Save this as demo.ts and run bun demo.ts (or DEMO_HEADLESS=1 bun demo.ts on a server). The example hosts its own fixture, uses your ElevenLabs account for narration, and writes .demo-artifacts/quick-start/crm-demo.mp4 and its manifest. No product login is needed.

Render narration before recording to avoid network pauses. The runner waits for its duration; it does not play audio through your speakers. Close the session to finalize its video, attach that recording to the report, compose the MP4, and only then write the manifest. For your own product, replace the fixture URL and selectors and add a sign-in profile below.

TS
import { mkdir } from "node:fs/promises";
import { createDemoRunner, goto, narrate, spotlight, writeDemoManifest } from "@absolutejs/demo";
import { createPlaywrightDemoSession } from "@absolutejs/demo/playwright";
import { createElevenLabsVoiceover } from "@absolutejs/demo/voiceover";
import { composeDemoWithFFmpeg } from "@absolutejs/demo/composition";

const apiKey = process.env.ELEVENLABS_API_KEY;
if (!apiKey) throw new Error("Set ELEVENLABS_API_KEY in your environment or .env file.");
const outputDir = ".demo-artifacts/quick-start";
await mkdir(outputDir, { recursive: true });

// Render before recording so network latency does not leave dead air.
const text = "Here is the live pipeline view. Revenue at risk is forty-two thousand dollars.";
const voiceover = createElevenLabsVoiceover({ apiKey, outputDir });
const narration = await voiceover.speak({ text });

// A local fixture makes this runnable without an existing app or login.
const server = Bun.serve({
  hostname: "127.0.0.1",
  port: 0,
  fetch: () => new Response(
    '<!doctype html><html lang="en"><title>CRM demo</title><body style="font:24px sans-serif;padding:80px"><h1>Pipeline</h1><p id="total">Revenue at risk: $42,000</p></body></html>',
    { headers: { "content-type": "text/html" } }
  )
});

try {
  const session = await createPlaywrightDemoSession({
    headless: process.env.DEMO_HEADLESS === "1",
    recordVideoDir: outputDir,
    contextOptions: { viewport: { width: 1280, height: 720 } }
  });
  let closed = false;
  try {
    const runner = createDemoRunner({
      browser: session.browserDriver,
      annotations: session.annotations,
      voiceover: { speak: async () => narration },
      voiceoverPlayback: "wait-for-duration"
    });
    const report = await runner.run({
      id: "crm-demo",
      steps: [
        goto(server.url.href),
        spotlight({ selector: "#total", label: "Revenue at risk" }),
        narrate(text)
      ]
    });
    // Closing finalizes the video; the session does not attach it for you.
    const recording = await session.close();
    closed = true;
    if (recording) report.artifacts.push(recording);
    if (report.status === "completed") {
      report.artifacts.push(await composeDemoWithFFmpeg(report, {
        outputPath: outputDir + "/crm-demo.mp4",
        voiceoverTiming: "timeline"
      }));
    }
    await writeDemoManifest(report, outputDir + "/crm-demo.manifest.json");
    if (report.status !== "completed") throw new Error(report.error?.message ?? "Demo failed");
    console.log("Created", outputDir + "/crm-demo.mp4");
  } finally {
    if (!closed) await session.close();
  }
} finally {
  await server.stop(true);
}

runner.run() never throws for a failed demo — it returns a report with status: 'failed' and the error, after stopping the recorder so the partial recording survives. writeDemoManifest() then turns the report into a proof-of-run JSON document.

#Playwright Session

createPlaywrightDemoSession() (from @absolutejs/demo/playwright) launches the browser, opens a context + page, and hands back the two drivers the runner needs: a DemoBrowserDriver and an annotation driver. Playwright itself is loaded dynamically — it stays an optional dependency.

OptionTypeDescription
headlessbooleanDefaults to false — a demo is meant to be seen (and recorded).
browser'chromium' | 'firefox' | 'webkit'Engine to launch. Defaults to chromium.
channelstringBranded browser channel (e.g. "chrome") instead of the bundled build.
browserInstanceBrowserReuse an already-launched Playwright browser; close() then leaves it running.
contextOptionsRecord<string, unknown>Passed straight to browser.newContext() — viewport, locale, permissions.
recordVideoDirstringEnables Playwright video for the context; close() returns it as a recording artifact.
screenshotDirstringWhere screenshot() steps write their PNG files.
fullPageScreenshotsbooleanCapture the full scroll height (default true) instead of the viewport.
accountDemoCredentialProfileA storage-state profile applied when the context is created — start signed in.
TS
import { createPlaywrightDemoSession } from '@absolutejs/demo/playwright';

const session = await createPlaywrightDemoSession({
	browser: 'chromium', // 'chromium' | 'firefox' | 'webkit'
	channel: 'chrome', // branded Chrome instead of the bundled build
	headless: false,
	recordVideoDir: '.demo-video', // Playwright video for this context
	screenshotDir: '.demo-shots',
	fullPageScreenshots: true,
	contextOptions: { viewport: { width: 1600, height: 900 } },
	// A storage-state profile applied when the context is created.
	account: {
		id: 'returning-user',
		kind: 'storage-state',
		storageState: '.demo-auth/ae.storage-state.json'
	}
});

// The session exposes everything the runner needs:
session.browserDriver; // DemoBrowserDriver — goto/click/fill/type/press
session.annotations; // overlay annotation driver for this page
session.page; // the raw Playwright page for anything bespoke
const recording = await session.close(); // finalize video and close browser
if (recording) report.artifacts.push(recording); // attach before composing

// Lower-level pieces are exported too:
import {
	createPlaywrightAnnotationDriver,
	createPlaywrightDemoBrowser,
	launchPlaywright
} from '@absolutejs/demo/playwright';

const browser = await launchPlaywright({ headless: true });
const driver = createPlaywrightDemoBrowser({ page, screenshotDir: 'x' });
const annotations = createPlaywrightAnnotationDriver(page);

#Runner & Script Steps

createDemoRunner() holds the drivers; demoScript() declares what happens. Every driver is optional — a script that only narrates needs only a voiceover, and a step whose driver is missing fails with an error naming it.

OptionTypeDescription
browserDemoBrowserDriverExecutes goto/click/fill/type/press/waitFor/screenshot steps.
authDemoAuthDriverResolves signIn("<id>") steps against the script’s profiles.
annotationsDemoAnnotationDriverDraws spotlight / circle / highlight overlays.
desktopDemoDesktopDriverExecutes openApp / focusApp / hotkey / typeText / click steps.
recorderDemoRecorderStarted before step one; stopped (and collected as an artifact) after the last step or on failure.
voiceoverDemoVoiceoverspeak() target for narrate steps; returned artifacts join the report.
annotationFailure'throw' | 'continue'With "continue", a failed overlay becomes a log artifact and the run keeps going.
onEvent(event) => void | Promise<void>Streams run.started, step.started, step.completed, artifact, run.completed, run.failed.
voiceoverPlayback'continue' | 'wait-for-duration'Block each narration for its rendered audio duration so live playback stays in sync.
idFactory() => stringCustom run ids; defaults to a timestamped random id.
TS
import { createDemoRunner } from '@absolutejs/demo';

const runner = createDemoRunner({
	annotations: session.annotations, // overlay callout driver
	auth: createDemoAuthDriver(), // resolves signIn('<id>') steps
	browser: session.browserDriver, // goto/click/fill/press/waitFor
	desktop: createMacDesktopDriver(), // native-app steps
	recorder: screenRecorder, // started before step 1, stopped after
	voiceover: elevenLabs, // narrate(...) target

	// 'throw' (default) or 'continue' — a failed overlay is logged as
	// an artifact and the run keeps going.
	annotationFailure: 'continue',
	// Stream run/step/artifact events as they happen.
	onEvent: (event) => sendToDashboard(event),
	// Block each narrate() until the rendered audio duration elapses,
	// so on-screen action stays in sync with live playback.
	voiceoverPlayback: 'wait-for-duration',
	idFactory: () => 'run-' + Date.now().toString(36)
});

const report = await runner.run(script);
// report — the DemoRunReport:
// {
//   id, scriptId, startedAt, endedAt, durationMs,
//   status: 'completed' | 'failed',
//   error?: { message, stack? },
//   artifacts: DemoArtifact[], // recordings, voiceovers, screenshots
//   events: DemoRunnerEvent[]  // run.started, step.started,
// }                            // step.completed, artifact, run.failed

Steps are plain JSON-friendly objects; the builders below construct them. Each accepts a trailing { id?, name? } options object that labels the step in the event log and timeline.

signIn(profileId)Run the named credential profile through the auth driver.
goto(url)Navigate the page.
click(selector)Click an element.
fill(selector, value)Set an input value instantly.
press(selector, key)Press a key in an element.
waitFor(target)Wait for a selector to appear, or sleep for a number of ms.
screenshot(name?)Capture the page; the PNG becomes a screenshot artifact.
narrate(input)Speak a line — a string or { text, voice?, emotion? }.
spotlight / circle / highlightDraw a presenter callout over the UI (see Annotations).
clearAnnotations()Remove the current overlay.
openApp / focusAppOpen or focus a native app via the desktop driver.
hotkey(keys) / typeText(text)Send OS-level keystrokes or type text into the focused app.
wait(ms)Pause the run.
markRecording(label)Drop a chapter marker on recorders that support mark().
{ run: (context) => ... }Escape hatch — arbitrary logic with the full DemoContext.
TS
import {
	click,
	demoScript,
	fill,
	goto,
	markRecording,
	narrate,
	press,
	screenshot,
	signIn,
	spotlight,
	wait,
	waitFor
} from '@absolutejs/demo';

const script = demoScript({
	id: 'crm-demo',
	title: 'CRM demo',
	metadata: { audience: 'enterprise', owner: 'sales-eng' },
	profiles: [
		/* credential profiles — see Authentication */
	],
	steps: [
		signIn('ae'), // run a credential profile by id
		goto('http://localhost:3000/pipeline'),
		waitFor("[data-demo='pipeline-total']"), // selector or ms
		click('#quarter-toggle'),
		fill('#search', 'Acme'),
		press('#search', 'Enter'),
		narrate({
			text: 'Here is the live pipeline view.',
			emotion: 'confident'
		}),
		spotlight({
			selector: "[data-demo='pipeline-total']",
			label: 'Revenue at risk',
			durationMs: 1800
		}),
		markRecording('chapter: pipeline'), // recorder chapter marker
		screenshot('pipeline'),
		wait(1200),
		// Escape hatch — custom logic with the full DemoContext.
		{
			name: 'custom',
			run: async (context) => {
				await context.narrate('And one more thing.');
				await context.annotate({ type: 'clear' });
				context.addArtifact({ id: 'note', kind: 'log' });
			}
		}
	]
});

const report = await runner.run(script);

#Authentication

Sign-in is profile-based: declare named credential profiles on the script and trigger them with signIn("<id>") steps. Three profile kinds cover the common cases — your own AbsoluteJS app, any third-party login form, and a saved browser session.

Capabilityabsoluteformstorage-state
Works on sites you don’t control
Drives the real login UI
Navigates, types into fields, clicks submit
Posts to the @absolutejs/auth login route
/auth/login by default, or routes.login
Multi-step flows
username → Next → password via a steps array
Human-style typing
typeDelayMs types key-by-key; pressEnter submits
Success confirmation
success.selector and/or success.url
afterLoginUrln/a
Reuses a saved Playwright session
Applied when the browser context is created
Secrets resolved from env at sign-inn/a
Secrets never enter the script
Credentials are passed as env references ({ env: "VAR_NAME" }) — the runner resolves them at sign-in time, keeping credential values out of the script. This does not redact the screen: use demo accounts and review recordings and reports before sharing. A missing env var throws an error naming the variable, never its value. The sign-in log artifact records only which profile ran and whether it succeeded.
TS
import { createDemoRunner, signIn } from '@absolutejs/demo';
import { createDemoAuthDriver } from '@absolutejs/demo/auth';

const runner = createDemoRunner({
	// Default driver — handles all three profile kinds. routes.login
	// overrides the '/auth/login' default for absolute profiles.
	auth: createDemoAuthDriver({ routes: { login: '/api/auth/login' } }),
	browser: session.browserDriver
});

await runner.run({
	id: 'signin-demo',
	profiles: [
		// absolute — a site you own that uses @absolutejs/auth. Posts
		// email + password to the auth login route.
		{
			id: 'ae',
			kind: 'absolute',
			baseUrl: 'http://localhost:3000',
			email: { env: 'DEMO_EMAIL' },
			password: { env: 'DEMO_PASSWORD' },
			afterLoginUrl: 'http://localhost:3000/pipeline'
		},
		// storage-state — reuse a saved Playwright session; applied
		// when the browser context is created.
		{
			id: 'returning-user',
			kind: 'storage-state',
			storageState: '.demo-auth/ae.storage-state.json'
		}
	],
	// Trigger a profile from a step by id:
	steps: [signIn('ae')]
});

form profiles drive the real login UI of any site, including ones you don't control: navigate to loginUrl, fill fields, click submitSelector, and confirm via a success selector and/or URL. For bespoke auth screens, implement your own DemoAuthDriver.

TS
// A third-party site you do NOT control — drives the real login UI.
{
	id: 'saucedemo',
	kind: 'form',
	loginUrl: 'https://www.saucedemo.com/',
	fields: [
		{ selector: '#user-name', value: { env: 'SAUCE_USERNAME' } },
		{
			selector: '#password',
			value: { env: 'SAUCE_PASSWORD' },
			typeDelayMs: 60 // type key-by-key, like a human
		}
	],
	submitSelector: '#login-button',
	// Confirm the login worked — an element that appears and/or a URL.
	success: { selector: '.inventory_list' }
}

// Multi-step flows (username → Next → password) use a steps array:
{
	id: 'workspace',
	kind: 'form',
	steps: [
		{ action: 'goto', url: 'https://accounts.example.com/signin' },
		{
			action: 'fill',
			selector: '#identifier',
			value: { env: 'DEMO_EMAIL' }
		},
		{ action: 'click', selector: '#identifier-next' },
		{ action: 'waitFor', target: '#password' },
		{
			action: 'fill',
			selector: '#password',
			value: { env: 'DEMO_PASSWORD' },
			pressEnter: true // submit with Enter instead of a click
		}
	],
	success: { url: '/dashboard' }
}

// Bespoke auth screens: implement DemoAuthDriver yourself.
const customAuth: DemoAuthDriver = {
	signIn: async (profile, context) => {
		await context.browser?.goto('https://app.example.com/sso');
		await context.browser?.click('#continue-with-okta');
		await context.browser?.waitFor('.dashboard');
	}
};

#Annotations

Annotations are presenter-style callouts drawn as a fixed overlay above the page (max z-index, pointer events off — they never intercept the demo's own clicks). The Playwright annotation driver renders them in-page; any environment can implement DemoAnnotationDriver with a single show() method.

type"spotlight" dims the page around the target; "circle" rings it; "highlight" borders it; "clear" removes the overlay.
selectorCSS selector to anchor on — or use x / y / width / height for an explicit rect.
labelPresenter caption rendered above the target.
colorAccent color; defaults to #ff3b30.
durationMsAuto-clear after this many ms; omit to keep the callout up until the next one.
TS
import {
	circle,
	clearAnnotations,
	highlight,
	spotlight
} from '@absolutejs/demo';

// Spotlight — dims the rest of the page and glows around the target.
spotlight({
	selector: "[data-demo='pipeline-total']",
	label: 'Revenue at risk',
	durationMs: 1800 // auto-clears; omit to keep it up
});

// Circle — a rounded ring around an element, no page dimming.
circle({ selector: '#submit', color: '#ff3b30' });

// Highlight — border-only emphasis; the page stays fully visible.
// Targets are a selector OR an explicit x/y/width/height rect.
highlight({ x: 120, y: 240, width: 320, height: 48, label: 'Drop zone' });

// Remove whatever is currently drawn.
clearAnnotations();

#Desktop Control

Native-app automation goes through createCommandDesktopDriver() (from @absolutejs/demo/desktop) — each capability maps to a CLI command. On macOS, createMacDesktopDriver() ships ready-made: it opens and focuses apps with open -a and sends keystrokes via osascript. Linux and Windows provide equivalent factories with xdotool, wmctrl, PowerShell, or a UIA bridge.

open(target)Launch an app. Targets are a name string or { name, bundleId, executable, windowTitle }.
focus(target)Bring an app’s window to the front.
hotkey(...keys)Send a key chord (modifiers first, key last).
typeText(text)Type text into the focused app.
click?(x, y)Optional — click at screen coordinates.
waitForWindow?(target, timeoutMs?)Optional — block until a window exists.
TS
import { createDemoRunner, focusApp, openApp, wait } from '@absolutejs/demo';
import {
	createCommandDesktopDriver,
	createMacDesktopDriver
} from '@absolutejs/demo/desktop';

// macOS: open/focus apps and send keystrokes via osascript.
const runner = createDemoRunner({ desktop: createMacDesktopDriver() });

await runner.run({
	id: 'discord-demo',
	steps: [openApp('Discord'), wait(1000), focusApp('Discord')]
});

// Linux / Windows: map each capability to a command — xdotool, wmctrl,
// PowerShell, or a UIA bridge. Unconfigured capabilities throw with a
// message naming the missing capability.
const linuxDesktop = createCommandDesktopDriver({
	open: (target) => ['xdg-open', String(target)],
	focus: (target) => ['wmctrl', '-a', String(target)],
	hotkey: (keys) => ['xdotool', 'key', keys.join('+')],
	typeText: (text) => ['xdotool', 'type', text],
	click: (x, y) => [
		'xdotool', 'mousemove', String(x), String(y), 'click', '1'
	]
});