Open Form

Responses & export

Where responses are stored, how to browse them in Studio, and how to get them out.

By default responses are stored in SQLite, in a file inside your project:

.open-form/
├── open-form.db    # responses, form versions, Studio schedule overrides,
│                   # rate-limit state, signed-out Studio sessions
└── secret          # signs timing tokens and Studio sessions, hashes visitor ids

There's no database server to run. Open Form uses Node's built-in node:sqlite. Change the path with storage.path in open-form.config.ts or the OPEN_FORM_DB environment variable; the secret file moves with the database. Setting OPEN_FORM_SECRET replaces it.

Set OPEN_FORM_DATABASE_URL (or storage.url) and responses go to Postgres instead: the dev server, open-form start, Studio and every command below read and write there, and nothing is written to .open-form/open-form.db. Studio, search, filters, export and response numbers work the same on both.

Runtime data stays out of git

Your repo holds the questions, logic and policy. .open-form/ holds the answers, and open-form init adds it to .gitignore. Keep it that way: responses are personal data.

  • The .open-form/ directory is created 0700 and the database files 0600.
  • The dev server refuses to serve anything inside .open-form/ (or a custom database path), so the file can't be downloaded past Studio's sign-in.
  • On SQLite, deleting a response really erases it. SQLite's secure_delete is on and the write-ahead log is truncated afterwards, so no copy stays behind in either file. On Postgres a deleted response disappears from every query at once, but the old row stays on disk until VACUUM reclaims it, and backups keep it as long as your provider does (see Deleting responses).
  • Export files written with -o are readable by you only (0600). Name them responses*.csv, .xlsx or .json: the .gitignore that open-form init writes keeps those out of git at the workspace root, and the dev server refuses to serve them, along with Studio's <id>-responses-<date> downloads.

Response numbers

Each response gets a number within its form: Studio's Response #147. Numbers increase and aren't reused when you delete responses, even the latest one, so a webhook receiver doesn't see the same number twice. Deleting the form itself erases its numbering: a new form with the same id starts again at #1 (see open-form delete).

In Studio

Open a form and choose Responses.

  • The table shows the newest responses first, 50 per page, with their number, submission time and the first five questions every respondent sees. Columns picks which questions the table shows; the choice is kept per form in your browser, and Reset to default goes back. On a phone the table becomes a list of cards. New responses appear on their own while the tab is open (it checks every few seconds and when you come back to it), briefly highlighted.
  • Search matches answer values only, not field names, so searching "email" doesn't hit every row.
  • Filter narrows the table the way a Notion database does. Choose a question (or Submitted) and a condition; each rule becomes a chip above the table. Click a chip to change it, or its × to remove it. With two or more rules, choose whether a response must match all of them or any one. Filters work together with search, and the count shows how many of the form's responses are left. A filtered question's column stays in the table.
  • Sort from a column heading: click it for Sort ascending, Sort descending or Filter, plus Remove sort on the sorted column. The sorted column shows an arrow and stays in the table, and a chip above the table reverses the order when clicked; its × goes back to newest first. Text sorts A–Z regardless of case, numbers by value, and select, radio and checkbox-group answers in the order of the form's options. Empty answers come last either way; responses with the same answer stay newest first.
  • Click a row to open the response at /studio/:formId/responses/:responseId. The detail shows every answer with its question, when it was submitted and in which language. Copy copies one answer; Copy all copies the whole response as question and answer pairs. The arrows in its header, or ↑ / ↓ (K / J), step to the response above or below on the page. Ratings and other short number scales read as 4 / 5.
  • Delete removes one response after a confirmation. Select rows to delete several (Shift-click ticks a range), or select every response on the page, every response, or every response matching your search and filters. In a public read-only Studio, visitors who haven't signed in can search, filter, open and export responses, but see no checkboxes or Delete.

Deleting cannot be undone. Export first if you might need the data.

Filter conditions

QuestionConditions
Text, email, URL, phone, long textcontains, does not contain, is, is not, starts with, ends with, is empty, is not empty. Case doesn't matter.
Number=, ≠, >, <, ≥, ≤, is empty, is not empty
Select, radio, matrix rowis, is not — pick one or more options; is empty, is not empty
Checkbox grouphas any of, has all of, has none of, is empty, is not empty
Checkboxis checked, is not checked
Dateis on, is before, is after, is on or before, is on or after, is between, is empty, is not empty
Submittedin the past N days (1, 7, 30, 90 or your own), is on, is before, is after, is on or before, is on or after, is between

A rule without a value yet (a dashed chip) doesn't filter. Submitted days are days in your browser's time zone, like the times in the table. Filters belong to the page: they reset when you open another form, along with the sort.

Export

In Studio's Responses tab, Export offers Excel, CSV and JSON. While a search or filter narrows the table, the file holds the same responses; switch the menu to All to export every response instead. From the terminal, which always exports every response:

npx open-form responses export meetup                    # CSV to stdout
npx open-form responses export meetup -o responses.xlsx  # format from the extension
npx open-form responses export meetup --format json -o responses-meetup.json
FlagEffect
-o, --out <file>Write to a file instead of stdout. .xlsx and .json pick the format.
--format <format>csv, xlsx or json. Overrides the extension.

Excel files are binary, so xlsx needs -o when stdout is a terminal. The CLI reads the database file directly and loads the form the way the server does, so it doesn't need a running server and writes the same file as Studio.

FormatWhat you get
ExcelNumbers as numbers, <DateInput> answers as dates, Submitted at as a date and time in the time zone named in its header, a bold frozen header row with filters.
CSVUTF-8 with a BOM so Excel opens accents and CJK correctly. Cells that a spreadsheet would run as formulas are escaped.
JSONOne object per response: id, number, submittedAt, locale (when one was recorded) and data, keyed by field name.

Every export lists responses oldest first. Tables start with # and Submitted at, then one column per field, headed by its label; fields that share a label are headed label (name). When the form has translations and at least one response recorded its language, Excel and CSV add a Language column after Submitted at, naming each language (English, 繁體中文). Both show Submitted at in the form's time zone, including one changed in Studio's Settings, and name it in the header (Submitted at (Asia/Taipei)); CSV writes it as 2026-10-30 17:00:00. Checkboxes read Yes or No.

Schema versions

Open Form derives each form's schema from its code, and every response is stored with the schema version it was submitted against. Old responses keep their questions after you edit the form.

Columns come from the newest version first, in form order, then fields only older versions had. A field's name is its column, so renaming a field splits its column: old responses keep the old name and new ones fill the new column.

// Keeps the column: only the question changes.
<Email name="email" label="Work email" required />

// Splits it: answers before and after the rename land in different columns.
<Email name="workEmail" label="Work email" required />

Change the label, not the name, once a field has responses.

What's next

On this page