Responses & export
Where responses are stored, how to browse them in Studio, and how to get them out.
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 idsThere'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. Setting OPEN_FORM_SECRET replaces the secret file.
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 created0700and the database files0600. - 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. - Deleting a response really erases it. SQLite's
secure_deleteis on and the write-ahead log is truncated afterwards, so no copy stays behind in either file. - Export files written with
-oare readable by you only (0600). Name themresponses*.csv,.xlsxor.json: the.gitignorethatopen-form initwrites 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 few answered questions. Open a response or export to see every answer.
- 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. 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. - 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.
Deleting cannot be undone. Export first if you might need the data.
Filter conditions
| Question | Conditions |
|---|---|
| Text, email, URL, phone, long text | contains, 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 | is, is not — pick one or more options |
| Checkbox group | has any of, has all of, has none of, is empty, is not empty |
| Checkbox | is checked, is not checked |
| Date | is on, is before, is after, is on or before, is on or after, is between, is empty, is not empty |
| Submitted | in 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, and exports always contain every response, oldest first.
Export
In Studio's Responses tab, Export offers Excel, CSV and JSON. From the terminal:
pnpm exec open-form responses export meetup # CSV to stdout
pnpm exec open-form responses export meetup -o responses.xlsx # format from the extension
pnpm exec open-form responses export meetup --format json -o responses-meetup.json| Flag | Effect |
|---|---|
-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.
| Format | What you get |
|---|---|
| Excel | Numbers 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. |
| CSV | UTF-8 with a BOM so Excel opens accents and CJK correctly. Cells that a spreadsheet would run as formulas are escaped. |
| JSON | One 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. CSV gives Submitted at as a UTC ISO timestamp
(2026-10-30T09:00:00.000Z). Excel shows it in the form's time zone, including
one changed in Studio's Settings.
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.