Building a playwright browser automation pipeline sounds like a solved problem until you’re actually inside one that has to do things no test suite was designed to do. This isn’t about running assertions or generating coverage reports. The pipeline I built was driving real browser sessions across multiple platforms, managing login state, handling anti-bot countermeasures, and outputting structured file artifacts on a schedule. If you’re already using Playwright for testing, this is the next layer: what happens when you take the tool outside the test runner entirely and make it do operational work. The architecture I ended up with works. Getting there required understanding exactly why the two tools I know best, Cypress and Selenium, would have collapsed under these specific requirements.
This isn’t a tools ranking post. It’s a record of a real build decision and the structural reasoning behind it.

What the pipeline actually needed to do
The pipeline’s job was to automate browser-based publishing tasks across multiple platforms without triggering automated-traffic detection. Each platform run required its own browser context with isolated session state, custom user agent strings, and specific flags to suppress automation fingerprints. The pipeline needed to handle dynamic page elements, wait on network conditions, and log structured output back to a queue file after each run. It also needed to be orchestrated externally, triggered by a scheduler rather than a test runner, and run silently in a headless environment.
These requirements aren’t exotic. Any engineer who’s built something that touches real web platforms at scale has hit most of these. The friction comes from the fact that most browser automation tools are designed around the test runner paradigm: a test suite triggers the browser, assertions validate state, the runner reports results. That model bakes assumptions into the tool that are fine for testing and wrong for pipeline work.
Why Cypress hits a structural ceiling here
Cypress is a genuinely good testing tool. The developer experience is excellent, the test runner is fast, and the built-in retry logic saves real time during test authoring. None of that translates to what this pipeline needed.
The first hard ceiling is architecture: Cypress runs inside the browser. It doesn’t control the browser from outside. It executes alongside your application in the same runtime. That design decision gives it speed and debugging clarity for test purposes, and it makes it fundamentally unsuitable for controlling browser state programmatically from an external orchestrator. You can’t hand Cypress a trigger from an n8n workflow and tell it to open a context, authenticate, do a thing, and return a file path. The tool isn’t built for that handoff.
The second ceiling is multi-context isolation. Each platform in the pipeline needed its own browser context, with separate cookie jars, separate session state, and no bleed between runs. Cypress operates on a single origin per test by default. The cross-origin limitations exist for sensible security reasons inside a test framework. Inside a pipeline that is explicitly moving between platforms, those same limitations are blockers with no clean workaround. You can patch around them, but at that point you’re fighting the tool’s assumptions on every run.
The third issue is the runner dependency itself. Cypress wants to be the thing that starts and stops your browser session. Externalizing that control requires restructuring how Cypress thinks about test lifecycle in ways that create more maintenance surface than just using a tool that was built for programmatic control.
Why Selenium is the wrong kind of flexible
Selenium doesn’t have Cypress’s architectural limitations. It controls the browser from outside, handles multiple contexts, and can be triggered from any orchestration layer. It’s also fifteen years of accumulated flexibility that you have to manage every time you use it.
The WebDriver protocol adds latency that compounds over a long pipeline run. Each command is a round-trip, a serialized instruction sent to a driver process that sends it to the browser and waits for confirmation. For a test suite running a few hundred assertions, this is fine. For a pipeline running multi-step platform interactions across several contexts per session, that latency adds up into something you notice and something that interacts badly with platforms that expect human-paced interaction timing.
Driver management is the other tax. WebDriver requires keeping browser drivers in sync with browser versions, which is infrastructure overhead that doesn’t pay anything forward. Playwright handles its own browser binaries. This seems minor until you’re maintaining a pipeline in a CI or local headless environment and a Chrome update breaks your driver version at an inconvenient time.
The stealth problem is the more serious one for this specific pipeline. Selenium sets navigator.webdriver = true by default. Removing that flag requires explicit patching, CDP commands, custom capabilities, and additional setup per run. Playwright’s --disable-blink-features=AutomationControlled flag and clean userAgent configuration give you a workable baseline without reconstructing the stealth layer from scratch for every context.
What made Playwright fit the requirements
The playwright browser automation pipeline works cleanly because Playwright was designed around the assumptions this use case actually has. It controls the browser from outside the page. It handles multiple browser contexts natively, with isolation built into the API. It runs headless without special configuration. And it exposes programmatic control that integrates naturally with an external orchestrator: pass it a trigger, get back a result, move on.
The browserContext API is the specific feature that makes multi-platform pipeline work tractable. Each context is a full isolated browser environment with its own cookies, its own session storage, and its own authentication state. Spinning up a fresh context per platform run costs almost nothing and gives you clean state every time without teardown complexity. Cypress can’t do this by design. Selenium can approximate it with more setup than it should require.
Here’s the minimal shape of a context spin-up with stealth flags, triggered externally rather than from a test runner. In the actual EchoCast pipeline this lives in a shared factory module so every platform script pulls from the same stealth baseline:
js – // Reusable Stealth Context Factory
// playwrightFactory.js
import { chromium } from 'playwright';
async function createStealthContext(options = {}) {
const {
platform = 'default',
headless = true,
userAgent = null,
} = options;
const browser = await chromium.launch({
headless,
args: [
'--disable-blink-features=AutomationControlled',
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
],
});
const context = await browser.newContext({
viewport: { width: 1280, height: 720 },
userAgent: userAgent || undefined,
bypassCSP: true,
ignoreHTTPSErrors: true,
permissions: ['notifications'],
});
await context.addInitScript(() => {
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3] });
Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
window.chrome = window.chrome || {};
window.chrome.runtime = window.chrome.runtime || {};
});
const page = await context.newPage();
return { browser, context, page, platform };
}No test runner invokes this. It’s called by whatever sits above it in the pipeline — an n8n webhook, a cron job, a queue processor. Each platform script passes its own platform label and userAgent string. The factory handles the rest. One baseline, every context isolated, nothing shared between runs.
The anti-automation fingerprint handling is more complete out of the box. The combination of --disable-blink-features=AutomationControlled, overriding navigator.webdriver via addInitScript, and setting a real browser userAgent string gets you past basic bot detection without a third-party stealth library. For this pipeline’s needs, that baseline was enough. Selenium gets you to the same place but requires assembling the pieces yourself and re-verifying them when browser internals change.
Playwright’s async-first design also matters for pipeline use. The async/await model with page.waitForSelector, page.waitForLoadState, and network idle detection handles the dynamic page timing problems without the brittle sleep timers that Selenium automation tends to accumulate over time. You describe what you’re waiting for, not how long to wait, which makes the pipeline more resilient to page load variance across platforms.
Where Playwright still makes you earn it
Playwright is not a turnkey solution and it’s worth being specific about where the complexity lives. The stealth baseline described above gets you past simple bot detection. It does not get you past sophisticated fingerprinting. If you’re building something that needs to operate at scale against platforms that invest heavily in bot mitigation, you’ll hit a ceiling with vanilla Playwright and need to go further, or reconsider the approach entirely.
Error handling in a pipeline context requires explicit design. Playwright will throw on a failed selector or a timeout, but what happens next is your responsibility. A test runner absorbs failures and reports them. A pipeline running in a scheduler has no runner to catch unhandled rejections. Building robust try/catch patterns, logging failures back to your queue, and deciding what “retry” means for each platform interaction is real engineering work that the tool doesn’t do for you.
Multi-step platform flows that require human-like timing need deliberate pacing logic if the platform uses behavioral analysis for bot detection. Playwright’s interaction model is fast and clean. Sometimes too clean, if the platform expects the microsecond variance of a real user’s mouse movement.
The honest verdict
Cypress is a better testing tool than Playwright for many test automation contexts. Selenium is more portable and has a longer ecosystem tail. Neither of those facts were relevant to this build because the build was never a test suite.
The right tool for a playwright browser automation pipeline is Playwright because Playwright was designed for programmatic browser control, not for test-runner-first browser automation. That distinction sounds semantic until you’re three hours into patching Cypress’s cross-origin restrictions or diagnosing a Selenium driver version mismatch in a headless environment at midnight. Know what the tool is for. Build with the one that matches your actual requirements.




