Agent-readable docs index: /docs/llms.txt. Full docs in one file: /docs/llms-full.txt. Download /docs/docs.zip to grep all markdown files locally.

Workflow

Define and run reusable browser automation workflows.
The workflow() factory is the entry point for every Libretto automation. You wrap your Playwright logic in a typed handler, define Zod schemas for the workflow input and output, export the result, and the CLI (or your own runner) takes care of launching a browser, validating input, wiring up context, and invoking your handler.

workflow()

Creates a named LibrettoWorkflow from input/output Zod schemas and a handler function.
function workflow< InputSchema extends z.ZodType, OutputSchema extends z.ZodType, >( name: string, schemas: { input: InputSchema; output: OutputSchema; }, handler: LibrettoWorkflowHandler< z.infer<InputSchema>, z.infer<OutputSchema> >, ): LibrettoWorkflow<InputSchema, OutputSchema>;
The handler receives a LibrettoWorkflowContext and typed input, and must return a Promise<Output>. The input type is inferred from schemas.input; the output type is inferred from schemas.output.
type LibrettoWorkflowHandler<Input, Output> = ( ctx: LibrettoWorkflowContext, input: Input, ) => Promise<Output>;

LibrettoWorkflowContext

sessionstring
The session identifier for this workflow run. Use it to correlate logs and state files.
pagePage
A Playwright Page instance ready for navigation and interaction.

Full example

import { workflow, launchBrowser, pause } from "libretto"; import { z } from "zod"; const inputSchema = z.object({ query: z.string(), }); const outputSchema = z.array(z.string()); export default workflow( "main", { input: inputSchema, output: outputSchema, startUrl: "https://example.com/search", }, async (ctx, input) => { const { page } = ctx; await page.fill("#query", input.query); await page.click("#submit"); await page.waitForSelector(".results"); const results = await page.$$eval(".result-item", (els) => els.map((el) => el.textContent ?? ""), ); return results; }, );

Running with the CLI

Pass the file path to npx libretto run. The file must have a default-exported workflow():
npx libretto run ./my-workflow.ts
The CLI compiles the file with tsx, launches a Chromium browser, and calls your handler with the session context it constructs.

Browser launch options

Always declare startUrl on every workflow. It is the entry URL Libretto opens before the handler runs when Libretto launches the browser (local Chromium or a provider session). Kernel and Libretto Cloud preload it at browser create time (before CDP attach). Browserbase and Steel open it immediately after CDP connect. run --cdp attaches to an existing page and does not navigate to startUrl.
export default workflow( "bookMarriott", { input: z.object({ checkIn: z.string() }), output: z.object({ confirmation: z.string() }), startUrl: "https://www.marriott.com/", gpu: true, viewport: { width: 1440, height: 900 }, }, async (ctx, input) => { // Browser is already on startUrl — do not page.goto the same URL here. }, );
path startUrlstring
Entry URL for the workflow. Declare this on every workflow. When Libretto launches the browser, it opens this URL before the handler runs. On Kernel, Libretto sends it as start_url at session create time and does not navigate again after connect. run --cdp leaves the existing page URL unchanged. Prefer this over an initial page.goto in the handler for launch and provider runs.
path gpuboolean
Request a GPU-accelerated browser when the provider supports it. Prefer declaring this per workflow; GPU is expensive to leave on by default.
path viewport{ width: number; height: number }
Browser viewport for the session. Hosted jobs and Kernel provider runs use this when you do not pass --viewport on the CLI.
These fields are stored in deploy metadata. Prefer declaring them on the workflow rather than repeating flags on every job. Until the hosted executor reads that metadata, pass the same values with libretto cloud jobs create --start-url ... --gpu --viewport WxH (or the matching schedule flags) when a hosted run needs them.

launchBrowser()

Launches a Playwright Chromium browser and returns a ready-to-use BrowserSession. Use this when you want to run workflows programmatically outside the CLI, or when you need direct access to the Browser or BrowserContext objects.
async function launchBrowser(args: LaunchBrowserArgs): Promise<BrowserSession>;

LaunchBrowserArgs

path sessionNamestringrequired
A unique identifier for this browser session. Used to name the session state file written under .libretto/sessions/.
path headlessboolean
Whether to run Chromium in headless mode. Defaults to false (visible window).
path viewport{ width: number; height: number }
The browser viewport size. Defaults to { width: 1366, height: 768 }.
path storageStatePathstring
Path to a Playwright storage state JSON file (cookies, localStorage). Useful for resuming authenticated sessions.

BrowserSession return value

browserBrowser
The underlying Playwright Browser instance.
contextBrowserContext
The Playwright BrowserContext created for this session.
pagePage
The initial Page opened in the context. Pass this to your workflow handler.
debugPortnumber
The remote debugging port Chromium is listening on.
metadataPathstring
Absolute path to the session state JSON file written by launchBrowser.
close() => Promise<void>
Closes the browser and releases all resources.

Programmatic usage

import { launchBrowser } from "libretto"; import main from "./my-workflow"; const session = await launchBrowser({ sessionName: "my-run", headless: true, }); try { const result = await main.run( { session: "my-run", page: session.page, }, { query: "hello world" }, ); console.log(result); } finally { await session.close(); }

pause()

Pauses a running workflow so you can inspect browser state interactively, then resume from the CLI.
async function pause(session: string): Promise<void>;
path sessionstringrequired
The session identifier. Must match the session name used when the workflow was started.
pause() is a no-op when NODE_ENV === "production". It is safe to leave pause() calls in your code without worrying about them blocking production runs.
When called in a non-production environment, pause() writes a .paused signal file and polls for a .resume signal. Use the Libretto CLI to send the resume signal:
npx libretto resume --session <session-name>

Example

import { workflow, pause } from "libretto"; import { z } from "zod"; export default workflow( "main", { input: z.object({ id: z.string() }), output: z.void(), }, async (ctx, input) => { const { page, session } = ctx; await page.goto(`https://example.com/items/${input.id}`); // Pause here to inspect the page before continuing await pause(session); await page.click("#confirm"); }, );