Define and run reusable browser automation workflows.
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()LibrettoWorkflow from input/output Zod schemas and a handler function.1234567891011121314function 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>;
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.1234type LibrettoWorkflowHandler<Input, Output> = ( ctx: LibrettoWorkflowContext, input: Input, ) => Promise<Output>;
LibrettoWorkflowContextsessionstringpagePagePage instance ready for navigation and interaction.123456789101112131415161718192021222324252627282930import { 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, }, async (ctx, input) => { const { page } = ctx; await page.goto("https://example.com/search"); 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; }, );
npx libretto run. The file must have a default-exported workflow():1npx libretto run ./my-workflow.ts
tsx, launches a Chromium browser, and calls your handler with the session context it constructs.launchBrowser()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.1async function launchBrowser(args: LaunchBrowserArgs): Promise<BrowserSession>;
LaunchBrowserArgspath sessionNamestringrequired.libretto/sessions/.path headlessbooleanfalse (visible
window).path viewport{ width: number; height: number }{ width: 1366, height: 768 }.path storageStatePathstringBrowserSession return valuebrowserBrowserBrowser instance.contextBrowserContextBrowserContext created for this session.pagePagePage opened in the context. Pass this to your workflow handler.debugPortnumbermetadataPathstringlaunchBrowser.close() => Promise<void>1234567891011121314151617181920import { 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()1async function pause(session: string): Promise<void>;
path sessionstringrequiredpause() 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.pause() writes a .paused signal file and polls for a .resume signal. Use the Libretto CLI to send the resume signal:1npx libretto resume --session <session-name>
1234567891011121314151617181920import { 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"); }, );