Quick answer: Page through every submission over HTTP, read aggregates instead of raw rows, export CSV, and pipe form data into dashboards and AI agent context.

Storing submissions is only half a backend. The half that decides whether your data is useful is getting it back out—for a human to browse, for a script to process, for a dashboard to chart, for an agent to reason over.

A FormLM endpoint gives you three exits, and picking the right one is most of the design. Here is the whole toolkit, with the loops and schedules that turn "collected data" into "working data."

The three exits

Same data, three shapes. A Monday-morning "how did the workshop go?" is a CSV open; a nightly sync into a spreadsheet archive is the query loop; an always-live results widget is a summary call and a radar chart.

Paging through everything, safely

The query endpoint paginates, so the correct pattern is a loop that stops on a short page—never a hard-coded "take page 5 and hope." Two lines of bash and jq pull the full history into a local file:

# fetch every stored record — stop when a page comes back short
page=1; out=all.jsonl; : > $out
while :; do
  resp=$(curl -s "https://formlm.me/api/v3/share/<your-token>/query?page=$page&size=1000")
  echo $resp | jq -c '.records[]?' >> $out
  n=$(echo $resp | jq '.records | length')
  [$n -lt 1000 ] && break
  page=$((page+1))
done

The same loop in Python or a platform script is a few more lines and the same idea. Keep size=1000—the per-page ceiling—for bulk pulls, and shrink it for interactive dashboards so a slow record never blocks the view. If you only ever need counts and distributions, skip the loop entirely: /summary already did that arithmetic server-side.

Aggregates without the raw rows

Dashboards rarely need every submission—they need "342 responses this week, dimension averages 3.8 / 2.9 / 4.1, band distribution 12% / 41% / 47%." The summary endpoint is exactly that trade: one GET, no pagination, nothing to join. Point a low-refresh widget at it and the entire reporting layer costs you a fetch and a chart.

Schema drift is a feature—read defensively

Because unknown payload keys auto-create their own columns, the field set of a live endpoint grows as your page evolves. Old records simply have nothing for new keys; that is not corruption, it is history. Three habits keep pipelines calm:

Scheduled exports to sheets and BI

For teams that live in spreadsheets, wrap the query loop in a nightly cron, write CSV or append rows to your sheet, and let the form stay boring infrastructure. There is a simpler tier for smaller needs—manual CSV export from the data UI—so you only automate when volume actually justifies it. And for BI tools that can hit an HTTP source, the query endpoint speaks their language directly: JSON with stable record IDs.

Feed the results back to an agent

The exit that surprises people: an agent can consume what the agent produced. Paste an endpoint's ?help into a conversation and the agent knows the schema; pipe in a /summary JSON and it can narrate this week of results; hand it the query loop output and it writes the report. Storage stops being the end of the form's life and becomes the start of an analysis loop—the same URL that collected the data is the one that explains it.

"A form is a data pipeline with one stage already built. The interesting engineering starts at the exits—pulling the same records toward humans, spreadsheets, dashboards, and agents without letting any two of them disagree about the truth."

🛠️ Give your data three exits

Every published form ships with all of them—no separate setup:

  • Query API — paginated JSON, up to 1,000 records per page
  • Summary API — aggregates computed server-side, one GET
  • Data UI + CSV — the human-first exit, included
  • ?help self-docs — the current field list, Markdown, agent-readable
Publish a form and pull your first page →

✅ Key Takeaways

  • Storage is half a backend; the exits decide whether the data works for you
  • Loop query until a page comes back short—size=1000 for bulk, smaller for interactive
  • Use /summary when you need aggregates; one GET beats downloading everything
  • Auto-created fields mean schema grows—treat missing keys as null, diff via ?help
  • Automate exports on demand: manual CSV is the default, cron the escalation
  • The same endpoint that stores the data can explain it to your AI agent

Working with benchmarks and reports rather than scripts? The B2B Marketing and Training Effectiveness tracks turn these same aggregates into client-facing deliverables.

Frequently Asked Questions

How do I download all submissions programmatically?

Call the query endpoint in a loop: GET /query with page=N and size=1000, append the records, and stop when a page returns fewer than the requested size. That is the whole pattern—no cursor state to track.

Is there a charge per API call?

No. Publishing a form and receiving or querying submissions is available on the free plan with no per-call charge; paid plans raise the volume ceilings and add team features.

How do I keep my script from breaking when I add fields?

Read defensively: iterate the union of keys and treat missing ones as null. Append ?help to your endpoint to fetch the current field list as Markdown, and diff it in your sync job so new columns never surprise you.

Can a dashboard read the data directly?

Yes—CORS is open on reads, so a client-side widget can hit the query and summary endpoints directly. Prefer /summary for aggregate tiles: one GET, no pagination, and the arithmetic already happened server-side.

← Previous: The MCP Walkthrough Next in docs: Data API Overview →