Docs
Dashboard
Examples / FormsBeta

Fill a form from a spreadsheet

Read rows from a CSV with Python in the shell, and fill one form per row with plain-English steps, pausing before the first submit so you can check it in the live view. The row values reach the steps as %name%-style variables: the model picks the fields but never sees the values.

When to use it: You have rows of data to type into a web form, and want to check the first one before it is sent.

Usesexec (curl, Python's csv), step actions with variables, the live view
Needsa plan with shell sessions and plain-English steps, and a model on the API
Siteyour sheet and form; the defaults are a demo CSV and httpbin.org's demo order form. The runner uses its stand-in
Outputoutput/result.json with what the page said after each submit
LanguagesNode and Python

You take part: It stops after filling the first row: check it in the live view, then press Enter.

The code

The example as it is in its folder. Both versions do the same job and write the same output/, so one result check covers both.

/** * Fill a form from a spreadsheet: read rows from a CSV with Python in the session's shell, and fill one form per row * with plain-English steps, pausing before the first submit so you can check it in the live view. * *   npx tsx node/index.ts            (BOXLINE_API_KEY; SHEET_URL: a CSV with name,phone,email columns; FORM_URL; ROWS) * * The rows' values go to the steps as %name%, %phone% and %email% variables: the model picks the fields, the values * are filled in on the server and never shown to it. Writes output/result.json with what the page said after each submit. */import { mkdirSync, writeFileSync } from "node:fs";import { join } from "node:path";import { createInterface } from "node:readline/promises";import { Boxline } from "@boxline/sdk";const out = process.env.OUTPUT_DIR ?? "output";// A demo CSV served by httpbin.org (it decodes the base64 in the address): Ada Lovelace, Alan Turing, Grace Hopper.const DEMO_SHEET =  "https://httpbin.org/base64/bmFtZSxwaG9uZSxlbWFpbApBZGEgTG92ZWxhY2UsKzEgNTU1IDAxMDAsYWRhQGV4YW1wbGUuY29tCkFsYW4gVHVyaW5nLCsxIDU1NSAwMTAxLGFsYW5AZXhhbXBsZS5jb20KR3JhY2UgSG9wcGVyLCsxIDU1NSAwMTAyLGdyYWNlQGV4YW1wbGUuY29tCg==";const sheetUrl = process.env.SHEET_URL ?? DEMO_SHEET;const formUrl = process.env.FORM_URL ?? "https://httpbin.org/forms/post";const maxRows = Number(process.env.ROWS ?? 3);const bx = new Boxline();const session = await bx.sessions.create({ shell: true, keepAlive: true, timeout: 900, userMetadata: { example: "form-from-spreadsheet" } });console.log(`Session: ${session.id}`);const rl = createInterface({ input: process.stdin, output: process.stdout });try {  // 1. The shell: download the sheet and read it with Python's csv module.  const r = await session.exec(    `mkdir -p data && curl -sSfL --max-time 60 -o data/sheet.csv "$SHEET_URL" && python3 -c 'import csv, json; print(json.dumps(list(csv.DictReader(open("data/sheet.csv")))))'`,    { env: { SHEET_URL: sheetUrl } },  );  if (r.exitCode !== 0) throw new Error(`could not read the sheet: ${r.stderr.trim()}`);  const rows = (JSON.parse(r.stdout) as { name: string; phone: string; email: string }[]).slice(0, maxRows);  console.log(`${rows.length} rows to fill from the sheet`);  // 2. One form per row: plain-English steps pick the fields; the values stay out of the model's sight.  const submitted: { name: string; pageSays: string }[] = [];  let modelUsd = 0;  for (const [i, row] of rows.entries()) {    await session.goto(formUrl);    const steps = await session.actions([      { action: "step", instruction: "type %name% into the customer name field", variables: { name: row.name } },      { action: "step", instruction: "type %phone% into the telephone field", variables: { phone: row.phone } },      { action: "step", instruction: "type %email% into the email address field", variables: { email: row.email } },    ]);    const failed = steps.find((s) => !s.ok);    if (failed) throw new Error(`a step failed: ${failed.error}`);    modelUsd += steps.reduce((a, s) => a + ((s.value as { usage?: { costUsd: number } })?.usage?.costUsd ?? 0), 0);    if (i === 0) {      console.log(`\nRow 1 is filled in. Check it in the live view (the link works like a password):\n  ${session.liveUrl}`);      await rl.question("Check the first row in the live view, then press Enter to submit it: ");    }    await session.click("form button, form [type=submit]");    await session.wait(1000);    const { content } = await session.content("text");    submitted.push({ name: row.name, pageSays: content.slice(0, 600) });    console.log(`Row ${i + 1} (${row.name}) submitted: ${content.replace(/\s+/g, " ").slice(0, 100)}`);  }  mkdirSync(out, { recursive: true });  writeFileSync(join(out, "result.json"), JSON.stringify({ sheetUrl, formUrl, rows: submitted, reviewed: true, usage: { modelUsd } }, null, 2));} finally {  rl.close();  await session.stop();}

Inputs

Environment variables the example reads:

  • SHEET_URL: a CSV with name, phone and email columns.
  • FORM_URL: the form (default https://httpbin.org/forms/post).
  • ROWS: how many rows (default 3).

Run it

Get the examples with git clone https://github.com/Boxline-dev/examples.git and open this one: cd examples/form-from-spreadsheet. Put your API key in BOXLINE_API_KEY (and BOXLINE_API_URL for another API), then run either version from the example’s folder. Both write to output/ and stop their sessions when they finish.

# Node 18 or newer, in the example's folderexport BOXLINE_API_KEY=…   # your API keynpm install @boxline/sdk tsxnpx tsx node/index.ts

Then check the result, from the same folder: npx tsx check.ts output. It exits with 0 only when the result is right.

What its check proves

check.ts reads what the example wrote and passes only when the result is right, not merely when the example ran.

On the stand-in, the form received rows 1 to 3 with the right phone and email (not row 4), the first only after the review.

Source

github.com/Boxline-dev/examples/form-from-spreadsheet on GitHub, with its README.md, check.ts and .env.example. The examples are open source. See all examples.

These docs describe a beta API. Endpoints, fields and SDK methods may change before general availability; breaking changes will be listed in the changelog.