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 with bun add @absolutejs/demo, and add optional drivers only when needed (bun add -d playwright). A browser demo is three pieces: a Playwright session (page + video + screenshots), a runner holding the drivers, and a declarative script of steps. The report that comes back carries every artifact and a timestamped event log.

TS
import {
	createDemoRunner,
	goto,
	narrate,
	signIn,
	spotlight,
	writeDemoManifest
} from '@absolutejs/demo';
import { createDemoAuthDriver } from '@absolutejs/demo/auth';
import { createPlaywrightDemoSession } from '@absolutejs/demo/playwright';

const session = await createPlaywrightDemoSession({
	headless: false,
	recordVideoDir: '.demo-video',
	screenshotDir: '.demo-shots'
});

const runner = createDemoRunner({
	auth: createDemoAuthDriver(),
	browser: session.browserDriver,
	annotations: session.annotations,
	voiceover: {
		speak: async ({ text }) => {
			console.log('[voiceover]', text);
		}
	}
});

const report = await runner.run({
	profiles: [
		{
			id: 'ae',
			kind: 'absolute',
			baseUrl: 'http://localhost:3000',
			email: { env: 'DEMO_EMAIL' },
			password: { env: 'DEMO_PASSWORD' },
			afterLoginUrl: 'http://localhost:3000/pipeline'
		}
	],
	id: 'crm-demo',
	title: 'CRM demo',
	steps: [
		signIn('ae'),
		narrate('Here is the live pipeline view.'),
		goto('http://localhost:3000/pipeline'),
		spotlight({
			selector: "[data-demo='pipeline-total']",
			label: 'Revenue at risk',
			durationMs: 1800
		})
	]
});

await writeDemoManifest(report, '.demo-artifacts/crm-demo.manifest.json');
console.log(report.status, report.artifacts);
await session.close();

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; // DemoBrowserDrivergoto/click/fill/type/press
session.annotations; // overlay annotation driver for this page
session.page; // the raw Playwright page for anything bespoke
await session.recordingArtifact(); // video as a recording artifact
await session.close(); // stop recording, close context + browser

// 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, so real secrets never enter the script object, the manifest, or the recording. 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'
	]
});