⛭ Typed browser automation for the BEAM ⛭

Package Version Hex Docs Target: Erlang


Chrobot provides a set of typed bindings to the stable version of the Chrome Devtools Protocol, based on its published JSON specification.

The typed interface is achieved by generating Gleam code for type definitions as well as encoder / decoder functions from the parsed JSON specification file.

Chrobot also exposes some handy high level abstractions for browser automation, and handles managing a browser instance via an Erlang Port and communicating with it for you.

You could use it for

🦝 The generated protocol bindings are largely untested and I would consider this package experimental, use at your own peril!



Chrobot can use an existing system installation of Google Chrome or Chromium, if you already have one.

If you would like a hermetic installation of a specific version of a chrome build optimized for automation, I recommend using the installation script from puppeteer to achieve this

# (you will need node.js to run this of course)
npx @puppeteer/browsers install chrome

The chrobot.launch / chrome.launch commands will attempt to find a local chrome installation like this, and prioritize it over your system installation.


Install as a Gleam package

gleam add chrobot

Install as an Elixir dependency with mix

# in your mix.exs
defp deps do
    {:chrobot, "~> 2.0.0", app: false, manager: :rebar3}


Take a screenshot of a website

import chrobot

pub fn main() {
  // Open the browser and navigate to the gleam homepage
  let assert Ok(browser) = chrobot.launch()
  let assert Ok(page) =
    |>"", 30_000)
  let assert Ok(_) = chrobot.await_selector(page, "body")
  // Take a screeshot and save it as 'hi_lucy.png'
  let assert Ok(screenshot) = chrobot.screenshot(page)
  let assert Ok(_) = chrobot.to_file(screenshot, "hi_lucy")
  let assert Ok(_) = chrobot.quit(browser)

Generate a PDF document with lustre

import chrobot
import lustre/element.{text}
import lustre/element/html

fn build_page() {
  html.body([], [
    html.h1([], [text("Spanakorizo")]),
    html.h2([], [text("Ingredients")]),
    html.ul([], [[], [text("1 onion")]),[], [text("1 clove(s) of garlic")]),[], [text("70 g olive oil")]),[], [text("salt")]),[], [text("pepper")]),[], [text("2 spring onions")]),[], [text("1/2 bunch dill")]),[], [text("250 g round grain rice")]),[], [text("150 g white wine")]),[], [text("1 liter vegetable stock")]),[], [text("1 kilo spinach")]),[], [text("lemon zest, of 2 lemons")]),[], [text("lemon juice, of 2 lemons")]),
    html.h2([], [text("To serve")]),
    html.ul([], [[], [text("1 lemon")]),[], [text("feta cheese")]),[], [text("olive oil")]),[], [text("pepper")]),[], [text("oregano")]),
  |> element.to_document_string()

pub fn main() {
  let assert Ok(browser) = chrobot.launch()
  let assert Ok(page) =
    |> chrobot.create_page(build_page(), 10_000)

  // Store as 'recipe.png'
  let assert Ok(doc) = chrobot.pdf(page)
  let assert Ok(_) = chrobot.to_file(doc, "recipe")
  let assert Ok(_) = chrobot.quit(browser)

Scrape a Website

🍄‍🟫 Just a quick reminder:
Please be mindful of the load you are putting on other people’s web services when you are scraping them programmatically!

import chrobot
import gleam/io
import gleam/list
import gleam/result

pub fn main() {
  let assert Ok(browser) = chrobot.launch()
  let assert Ok(page) =
    |>"", 30_000)

  let assert Ok(_) = chrobot.await_selector(page, "body")
  let assert Ok(page_items) = chrobot.select_all(page, ".product_pod h3 a")
  let assert Ok(title_results) =, fn(i) { chrobot.get_attribute(page, i, "title") })
    |> result.all()
  let assert Ok(_) = chrobot.quit(browser)

Using from Elixir

# ( output / logging removed for brevity )
iex(1)> {:ok, browser} = :chrobot.launch()
iex(2)> {:ok, page} =, "", 10_000)
iex(3)> {:ok, object} =, "h1")
iex(4)> {:ok,text} = :chrobot.get_text(page, object)
iex(5)> text
"Example Domain"

Documentation & Guide

The full documentation can be found at

🗼 To learn about the high level abstractions, look at the chrobot module documentation.

📠 To learn how to use the protocol bindings directly, look at the protocol module documentation.

Search Document