# How to Send Custom Chrome DevTools Protocol (CDP) Commands with Zendriver

> Learn to send custom Chrome DevTools Protocol CDP commands with Zendriver. Utilize the cdp package and domain modules to send any CDP command effortlessly.

- Repository: [CDP Driver/zendriver](https://github.com/cdpdriver/zendriver)
- Tags: how-to-guide
- Published: 2026-02-27

---

**Zendriver exposes the full Chrome DevTools Protocol through the `cdp` package, allowing you to send any CDP command by passing generator objects from domain modules like `cdp.page` or `cdp.runtime` to the `Tab.send()` method.**

The `cdpdriver/zendriver` library provides a Python interface to control Chrome browser instances programmatically. When you need functionality beyond the high-level API, you can send custom Chrome DevTools Protocol (CDP) commands directly using the built-in `cdp` package generators and the `Tab.send` method.

## Understanding the CDP Architecture in Zendriver

### The Generator-Based Command Pattern

Zendriver represents every CDP domain (e.g., `page`, `runtime`, `dom`) as a Python module containing functions that return **generator objects**. According to the source code in [`zendriver/core/connection.py`](https://github.com/cdpdriver/zendriver/blob/main/zendriver/core/connection.py) (lines 38-44), these generators are understood by the low-level `Connection.send` implementation and forwarded to the browser via WebSocket.

This design means that calling `cdp.runtime.evaluate()` does not execute immediately—it returns a generator describing the command parameters and target method. The actual transmission happens when you pass this generator to a sending mechanism.

### The Four-Step Execution Flow

The typical flow for sending custom CDP commands involves these components:

1. **Create a `Tab` object** – This holds a `Connection` that manages a WebSocket to the Chrome target.
2. **Call a CDP method** – Invoking `cdp.page.navigate(url='...')` returns a generator describing the CDP command.
3. **Pass to `Tab.send`** – `Tab.send` forwards the generator to `Connection.send` (see implementation in [`zendriver/core/tab.py`](https://github.com/cdpdriver/zendriver/blob/main/zendriver/core/tab.py), lines 48-66).
4. **Handle the response** – `Connection.send` (lines 34-71 in [`zendriver/core/connection.py`](https://github.com/cdpdriver/zendriver/blob/main/zendriver/core/connection.py)) registers the request, ensures the necessary domain is enabled via `Connection._register_handlers` (lines 80-124), and awaits the decoded response or raises `ProtocolException` on error.

## Enabling Domains and Executing CDP Commands

Many CDP domains require explicit enablement before use. The following example demonstrates enabling the Runtime domain and evaluating JavaScript directly using raw CDP commands, as shown in [`docs/tutorials/tutorial-code/cdp-1.py`](https://github.com/cdpdriver/zendriver/blob/main/docs/tutorials/tutorial-code/cdp-1.py).

```python
import asyncio
import zendriver as zd
from zendriver import cdp

async def main() -> None:
    browser = await zd.start()
    tab = await browser.get("https://example.com")

    # Enable the Runtime domain (required for many evaluate calls)

    await tab.send(cdp.runtime.enable())

    # Execute a custom CDP command – evaluate a script in the page context

    result = await tab.send(
        cdp.runtime.evaluate(
            expression="document.title",
            return_by_value=True,
            await_promise=False,
        )
    )
    print("Page title:", result[0].value)  # result is a list of RemoteObjects

    await browser.stop()

if __name__ == "__main__":
    asyncio.run(main())

```

The `cdp.runtime.enable()` call returns a generator that activates the Runtime domain on the browser target. Subsequently, `cdp.runtime.evaluate()` constructs a generator describing the evaluation parameters, which `tab.send` transmits via the underlying `Connection.send` logic.

## Sending Advanced Custom CDP Commands

Because `Tab.send` accepts any generator produced by the `cdp` package, you can issue commands not wrapped by high-level Zendriver helpers. This enables advanced scenarios such as custom network interception, performance tracing, or low-level DOM manipulation.

```python
import asyncio
import zendriver as zd
from zendriver import cdp

async def main() -> None:
    browser = await zd.start()
    tab = await browser.get("https://example.com")

    # Directly send the Page.navigate command with a URL of your choice

    await tab.send(cdp.page.navigate(url="https://www.python.org"))

    # Wait for the navigation to finish

    await tab.wait_for_load_state("load")

    await browser.stop()

if __name__ == "__main__":
    asyncio.run(main())

```

Here, `cdp.page.navigate()` creates a generator for the `Page.navigate` command. By calling `await tab.send()`, you issue the navigation without relying on a dedicated high-level wrapper, while still benefiting from Zendriver's automatic target management and event handling.

## Summary

- Zendriver exposes all CDP domains through the `cdp` package, where each method returns a generator object describing the specific protocol command.
- Use `await tab.send()` to execute any CDP command by passing generators from modules like `cdp.page`, `cdp.runtime`, or `cdp.dom`.
- The underlying `Connection.send` method in [`zendriver/core/connection.py`](https://github.com/cdpdriver/zendriver/blob/main/zendriver/core/connection.py) (lines 34-71) handles WebSocket transmission, automatic domain enablement, and response decoding.
- This architecture allows you to mix high-level Zendriver APIs with low-level CDP commands for maximum flexibility in browser automation.

## Frequently Asked Questions

### What is the difference between Tab.send and Connection.send?

`Tab.send` provides the public API for sending CDP commands from a specific browser tab, implemented in [`zendriver/core/tab.py`](https://github.com/cdpdriver/zendriver/blob/main/zendriver/core/tab.py) (lines 48-66). It forwards generators to `Connection.send`, which contains the core WebSocket logic in [`zendriver/core/connection.py`](https://github.com/cdpdriver/zendriver/blob/main/zendriver/core/connection.py) (lines 38-44 and 34-71) that actually transmits the command to Chrome and manages the response lifecycle.

### Do I need to enable CDP domains manually before sending commands?

Yes, many CDP domains require explicit enablement before use. The `Connection._register_handlers` method (lines 80-124 in [`zendriver/core/connection.py`](https://github.com/cdpdriver/zendriver/blob/main/zendriver/core/connection.py)) manages domain state, but you should explicitly call enable methods like `cdp.runtime.enable()` or `cdp.network.enable()` before issuing commands that depend on those domains to ensure the browser target is ready to accept them.

### Can I mix custom CDP commands with Zendriver's high-level API?

Absolutely. Since `Tab.send` works with any CDP generator while high-level methods use the same underlying infrastructure, you can interleave raw CDP commands with conveniences like `tab.wait_for_load_state()` or `browser.get()` within the same script without conflicts.

### Where are the CDP domain modules defined?

The `cdp` package contains automatically generated Python modules for each CDP domain (e.g., [`zendriver/cdp/page.py`](https://github.com/cdpdriver/zendriver/blob/main/zendriver/cdp/page.py), [`zendriver/cdp/runtime.py`](https://github.com/cdpdriver/zendriver/blob/main/zendriver/cdp/runtime.py)). These modules define functions that return generators describing the specific CDP protocol commands, their parameters, and return types according to the Chrome DevTools Protocol specification.