How to enter data into a web app using the Axiom Chrome CDP API and code

The Chrome API is a cloud browser you drive over Chrome DevTools Protocol with Playwright or Puppeteer, so you can automate data entry from your own stack with no local Chromium to manage. This guide reads rows from a spreadsheet and, for each row, fills a web form, submits it, and verifies the submission succeeded. The pattern suits any CRM or web app that has no bulk-import API.

This guide uses the public demoqa practice form as a concrete, runnable target. Swap FORM_URL and the selectors for your own app.

Before you begin


  • Node.js 18 or later.
  • Playwright installed: npm install playwright.
  • An axiom.ai API key from the Code Dashboard, set as the AXIOM_API_KEY environment variable. Keep it in the environment, never hardcoded in the script — a committed token can be used to spend your runtime quota.
  • Your data as a CSV file, exported from the spreadsheet, or reachable through the Google Sheets API.
  • The CSS selectors for the form fields you'll fill. An input's id or name attribute is usually the most stable choice.

Read your rows


Read the spreadsheet in your own code. This example uses a CSV whose columns match the demoqa form fields. Swap in the Google Sheets API or a database query if you prefer.

import { readFileSync } from 'node:fs';

// contacts.csv columns: firstName,lastName,email,gender,mobile
function readRows(path) {
  const [header, ...lines] = readFileSync(path, 'utf8').trim().split('\n');
  const keys = header.split(',');
  return lines.map((line) => {
    const cells = line.split(',');
    return Object.fromEntries(keys.map((k, i) => [k.trim(), cells[i]?.trim()]));
  });
}

const rows = readRows('./contacts.csv');

A sample contacts.csv:

firstName,lastName,email,gender,mobile
Ada,Lovelace,ada.lovelace@example.com,Female,5551230001
Alan,Turing,alan.turing@example.com,Male,5551230002
Grace,Hopper,grace.hopper@example.com,Female,5551230003

Note: This split-on-comma reader is fine for clean data. For values that contain commas or quotes, use a CSV library such as csv-parse.

Connect


Connect over CDP to a cloud browser. The demoqa form is public, so there's no login step. If your form sits behind a login, sign in once before the loop and keep credentials in environment variables, never in the script.

import { chromium } from 'playwright';

const browser = await chromium.connectOverCDP(
  `wss://cdp-lb.axiom.ai/?token=${process.env.AXIOM_API_KEY}`
);

const context = browser.contexts()[0];
const page = context.pages()[0] ?? await context.newPage();

// If your form requires login, uncomment and adapt:
// await page.goto('https://your-app.example.com/login');
// await page.fill("input[name='email']", process.env.APP_USER);
// await page.fill("input[name='password']", process.env.APP_PASS);
// await page.click("button[type='submit']");
// await page.waitForLoadState('networkidle');

Note: connectOverCDP() returns a Playwright Browser handle — nothing else. There is no live-view URL or session ID to watch; the Chrome API is a programmatic driving interface. A session consumes runtime quota for its entire open lifetime, so always close it (see below).

Puppeteer works too: puppeteer.connect({ browserWSEndpoint: "wss://cdp-lb.axiom.ai/?token=..." }). This guide uses Playwright.

Fill and submit the form for each row


Loop the rows. For each one, navigate to the form, enter the values, submit, and wait for the page's own success signal. page.fill() types into an input by CSS selector, and page.click() submits.

const FORM_URL = 'https://demoqa.com/automation-practice-form';

async function addContact(page, row) {
  await page.goto(FORM_URL, { waitUntil: 'domcontentloaded' });

  await page.fill('#firstName', row.firstName);
  await page.fill('#lastName', row.lastName);
  await page.fill('#userEmail', row.email);

  // The gender radio inputs are overlapped by demoqa's ad iframes,
  // so click the visible label text instead of the input.
  await page.getByText(row.gender, { exact: true }).click();

  await page.fill('#userNumber', row.mobile);

  const submit = page.locator('#submit');
  await submit.scrollIntoViewIfNeeded();
  await submit.click();

  // Success shows a modal titled "Thanks for submitting the form".
  // Waiting on it is how you know the row was accepted, not just clicked.
  await page.locator('#example-modal-sizes-title-lg').waitFor({ timeout: 10000 });
}

Note: Two gotchas that apply to many real forms, not just demoqa:

  • Elements can be covered by overlays or ad iframes. When a direct click() is intercepted, click a visible label (getByText) or call scrollIntoViewIfNeeded() first.
  • Avoid waitForLoadState('networkidle') on pages with ads or long-polling — the network never goes idle and the wait times out. Wait for a concrete element that only appears on success instead.

Confirm it worked


Clicking submit is not proof of success. The reliable signal is an element that only the server renders after it accepts the data. On demoqa that's the confirmation modal, which echoes back every value you submitted — so you can assert the round trip, not just that a button was pressed.

// After the modal appears in addContact():
const title = await page.locator('#example-modal-sizes-title-lg').innerText();
const summary = await page.locator('.modal-body .table tbody tr').allInnerTexts();
console.log(title);            // "Thanks for submitting the form"
console.log(summary.join('\n'));

// Optional visual proof: screenshot just the modal.
await page.locator('.modal-content').screenshot({ path: `proof-${row.email}.png` });

For your own app, pick the equivalent signal: a success toast, a redirect to the new record's URL, or the new row appearing in a list. Assert on that — a bare page.click() returns before the server has confirmed anything.

Full script


import { chromium } from 'playwright';
import { readFileSync } from 'node:fs';

const FORM_URL = 'https://demoqa.com/automation-practice-form';

function readRows(path) {
  const [header, ...lines] = readFileSync(path, 'utf8').trim().split('\n');
  const keys = header.split(',');
  return lines.map((line) => {
    const cells = line.split(',');
    return Object.fromEntries(keys.map((k, i) => [k.trim(), cells[i]?.trim()]));
  });
}

async function addContact(page, row) {
  await page.goto(FORM_URL, { waitUntil: 'domcontentloaded' });

  await page.fill('#firstName', row.firstName);
  await page.fill('#lastName', row.lastName);
  await page.fill('#userEmail', row.email);
  await page.getByText(row.gender, { exact: true }).click(); // gender label, not the overlapped input
  await page.fill('#userNumber', row.mobile);

  const submit = page.locator('#submit');
  await submit.scrollIntoViewIfNeeded();
  await submit.click();

  // Verify the server accepted the row before moving on.
  await page.locator('#example-modal-sizes-title-lg').waitFor({ timeout: 10000 });
}

async function run() {
  const rows = readRows('./contacts.csv');
  const browser = await chromium.connectOverCDP(
    `wss://cdp-lb.axiom.ai/?token=${process.env.AXIOM_API_KEY}`
  );

  try {
    const context = browser.contexts()[0];
    const page = context.pages()[0] ?? await context.newPage();

    for (const row of rows) {
      try {
        await addContact(page, row);
        console.log(`Added ${row.email}`); // only prints after the success modal is confirmed
      } catch (err) {
        console.error(`Failed for ${row.email}: ${err.message}`);
      }
    }
  } finally {
    await browser.close(); // always close — sessions bill for their whole open lifetime
  }
}

run();

Run it with node data-entry.js and watch the console for per-row results. Each Added line prints only after that row's success modal was confirmed, so the output is a truthful record of what landed.

Next steps


  • Confirm each field selector in your browser's devtools before running at scale.
  • Add the fields demoqa leaves optional (subjects, hobbies, date of birth, address, state/city) if you need them — they don't block a successful submit.
  • Check your remaining quota with the dashboard if a run drains more runtime than expected; a leaked session that never closes keeps billing until it is reaped.
  • Read the Chrome API guide for connection options and browser controls.
  • Prefer no-code? Build the same flow visually with the Fill a form from a spreadsheet starter in the No-Code Tool.