A spreadsheet that refreshes
Finance does not want a dashboard. It wants a workbook — with its own columns, its own formulas, its own commentary — that is correct on the first working day of the month. Qrly ships a full spreadsheet engine bound directly to your database, where a refresh updates the data and leaves everything you built around it exactly where it was.
Workbook, engine, binding
A spreadsheet report is a stored workbook, a real formula engine and a live connection to your data. Take any one away and it stops being useful.
A full workbook per report
- Multiple sheets, default 200 rows × 26 columns
- Column widths, row heights, frozen panes
- Cells keyed A1 with value, formula and style
- Every cell carries an owning-data-source marker
- Stored as JSON in a side table
- The report list stays light — workbooks are loaded on demand
Written, not wrapped
- Own tokenizer, parser and evaluator, in the browser
- Roughly 500 worksheet functions
- Dynamic-array spilling and the full Excel error set
- Cycle detection reported as #CIRC!
- Excel number formats and date serials
- Excel limits — column XFD, 1,048,576 rows
Four ways to fill a block
- A saved question
- Stored QQL, recompiled per run for the dialect
- Ad-hoc SQL authored in Monaco
- An AI prompt, resolved to SQL once
- Per binding: sheet, anchor cell, headers, max rows, key columns
- Refresh-safe: your columns and rows survive
Where most BI spreadsheets quietly stop
Plenty of tools give you a grid. The difference is what happens when you type an equals sign.
Everything you would reach for by reflex
A1 references, ranges, whole-column references, cross-sheet references and named ranges — the vocabulary anyone who has used a spreadsheet already has. Workbook-aware functions such as ROW, INDIRECT and OFFSET behave as they should, which is what makes patterns copied from an existing model actually work rather than nearly work.
Evaluation is lazy where Excel is lazy: IF and IFERROR do not evaluate the branch they are not taking, so a guard around a division or a lookup does what a guard is supposed to do.
Cycles, errors and number formats
Circular references are detected and reported as #CIRC! rather than hanging the tab or silently producing a number. The complete Excel error set is implemented — #DIV/0!, #N/A, #NAME?, #NULL!, #NUM!, #REF!, #VALUE!, #CIRC! and #SPILL! — so errors propagate the way the person building the model expects them to.
Excel number formats and date serials are supported, which matters more than it sounds: a date that is secretly a string is the single most common reason a migrated model gives a different answer.
Roughly 500, with the awkward ones done properly
The function library covers the usual surface, and then explicitly overrides the ones where a naive implementation and Excel disagree: UNIQUE, SUBTOTAL and AGGREGATE, GROUPBY, TEXTBEFORE and TEXTAFTER, WRAPCOLS, PERCENTOF and the REGEX* family.
Those are the functions people notice. SUBTOTAL that ignores hidden rows, UNIQUE that spills correctly, TEXTAFTER that handles the delimiter-not-found case the way Excel does — get them wrong and every number downstream is wrong with them.
Dynamic arrays and structural edits
Dynamic-array spilling is supported, with #SPILL! when the spill range is blocked — so modern array formulas behave rather than collapsing to their first cell.
Inserting and deleting rows and columns rewrites every dependent formula across the workbook. This is the least glamorous feature on the page and the one whose absence ruins a spreadsheet fastest: a grid where inserting a row breaks forty formulas is a grid nobody will maintain.
Editing behaviour people already have in their fingers
Muscle memory is a feature requirement. Anything that behaves differently from Excel here is a bug report waiting to be filed.
| Grid capability | Behaviour | Why it matters |
|---|---|---|
| Virtualized rendering | Only visible cells are drawn | A large sheet scrolls instead of freezing the tab |
| Two-mode in-cell editing | Excel edit-vs-enter semantics, plus a formula bar as an alternate editor | Arrow keys move the cursor or the selection, as they should |
| Point mode | Arrows and clicks write references into a formula; Shift extends to a range; re-pointing replaces | Build a formula by pointing, never by typing coordinates |
| Autocomplete and reference outlining | Function autocomplete in the editor; live outlining of the ranges a formula touches | You can see what a formula reads before you commit it |
| Paste-special | All, values, formulas, formats — and transpose | Pasting values over formulas is a deliberate act, not an accident |
| Fill handle with series detection | Numbers, dates, weekday and month names, repeated text, reference-shifting formulas | Drag-fill guesses what you meant, the way it does in Excel |
| Structure | Frozen panes, merged cells, hidden rows and columns, insert, delete and drag-reorder with reference relocation | Reshaping a sheet does not break the formulas on it |
| Working with data in place | Sorting, filtering, find & replace, undo/redo, context menus, autofit, keyboard navigation | Nobody has to export to Excel to do the obvious thing |
The boring half, done anyway
Formatting is where a spreadsheet feels finished or unfinished. There is no clever way to do it — you either ship the controls or you do not.
Fonts, styling, borders, alignment
- Undo, redo, paint format
- Font family — Default, Sans, Serif, Mono — and size
- Bold, italic, underline, strikethrough
- Text colour and fill colour
- Borders: all, outer, top, bottom, left, right, none
- Horizontal and vertical alignment, wrap text, indent
- Merge cells, merge across rows, unmerge
Currency, percent, dates, presets
- Currency, percent, increase and decrease decimals
- General, #,##0, #,##0.00, red negatives
- 0.00%, scientific, fractions
- $#,##0.00 and €#,##0.00
- yyyy-mm-dd, d mmm yyyy, yyyy-mm-dd hh:mm, hh:mm:ss
- Plain text, and clear formatting
Structure, sorting, validation
- Row above or below, column left or right, delete rows and columns
- +100 rows, +10 columns, hide, unhide, fit columns to content
- Sort A→Z and Z→A, create and remove filter
- Freeze first row, first column, both, up to this cell, unfreeze
- Data validation and named ranges
- AutoSum (Alt+=): SUM, AVERAGE, COUNT, COUNTA, MAX, MIN — plus add a totals row or an averages row
17 presets, and a full rule dialog
One click for the thing you want ninety per cent of the time, and a proper editor for the other ten.
Seventeen one-click rules
Duplicates, unique values, top ten, bottom ten, above and below average, negatives in red, errors, blanks, dates today, dates in the last seven days, dates this month, a colour scale, data bars, and arrow, traffic-light and rating icon sets.
These exist because they are the rules people actually apply, and because making someone build "highlight duplicates" out of a custom formula is a small, daily insult.
Cell, colour scale, data bar, icon set
The full dialog offers four rule types. Colour scales can be two- or three-colour; data bars take a bar colour; icon sets can be driven by percent, percentile or absolute number, with reverse order and custom thresholds.
Cell rules cover comparisons, between, text tests, duplicate, unique, top or bottom N or per cent, above and below average, empty, error, and an extensive date-period set.
The escape hatch
Any rule can be a custom formula, evaluated by the same engine that evaluates the sheet. If you can express the condition as a formula, you can format on it — including conditions that read other columns, other sheets or named ranges.
That is what keeps the preset list from being a ceiling.
What a matching cell looks like
Matching cells can take a text colour, a fill, bold, italic, underline and strikethrough — the same styling vocabulary as manual formatting, so a conditionally formatted sheet and a hand-formatted one look like they belong to the same document.
Which matters when the workbook leaves the building as an XLSX attachment on a board pack.
Four ways to bind a block to your database
This is the part that makes it a BI feature rather than a toy. A block is a rectangle of the sheet owned by a query, with an anchor cell, an optional header row, a maximum row count and a set of key columns.
| Binding kind | What is stored | Use it when |
|---|---|---|
| QUESTION | A reference to a saved question | The number already exists and is governed elsewhere — edit it once, every workbook follows |
| VISUAL | Stored QQL, recompiled on every run | The block should follow the connection's dialect rather than being pinned to one engine's SQL |
| SQL | Ad-hoc raw SQL, authored in Monaco with schema completion | You want exactly this query, written by hand, and nothing between you and it |
| AI | A natural-language prompt, resolved to SQL once, then refreshed as plain SQL | Fastest way to a first block — with no token spend and no drift on later refreshes |
The difference between a workbook and a paste-over
Every BI tool can dump a result set into a grid. The question is what happens the second time.
Why offset-based refresh destroys real work
A naive refresh writes the new result into the same rectangle, cell by cell, by position. That is fine on day one, when the block is all there is. It stops being fine the moment someone inserts a margin column beside it, adds a commentary row, or writes a note next to a specific customer — because on the next refresh the query returns its columns in a different order, or one fewer, or the rows come back sorted differently, and everything that was carefully placed is now beside the wrong record.
The predictable result is that people stop refreshing. They copy the block into a new tab, do their work there, and the "live" report becomes a manual one within two months. Qrly addresses this by addressing source columns by name, not by offset.
- Your columns are preserved and never written. Columns and rows you inserted inside a block stay where they are. Qrly writes the source columns it owns, and steps over everything else.
- A dropped source column is cleared, not shifted. When a column disappears from the query, its cells are cleared in place. Nothing slides left, so no formula that pointed at the column to its right suddenly points at something else.
- New source columns append past the block. A query that gains a column extends the block outward rather than pushing your own columns aside.
- Key columns put each record back on its row. With key columns configured, a record returns to the row it occupied last time, whatever order the query produced. Your hand-written note stays beside its customer across reorders — which is the whole reason people put notes in spreadsheets rather than in the database.
- Stale source cells are removed; your cells are never touched. Cells carry an owning-data-source marker. Cells Qrly wrote and no longer owns are cleaned up. Untagged cells — everything you typed — are left alone, permanently.
- The refresh reports what it did. Rows and columns written, columns and rows kept, columns dropped and rows skipped. You do not have to diff the sheet to find out whether the refresh did something you did not expect.
Help with the sheet, and getting it out
A whitelist, not a free hand
The spreadsheet assistant works one-shot or as a multi-turn chat, and it can only propose operations from a fixed whitelist: setCells, setRange, addSheet, format and clearRange. It cannot invent an operation, and it is capped at 400 context cells and 200 operations per reply.
The cap is not only about cost. An assistant permitted to rewrite an unbounded region of a financial model is a category of accident you do not want available.
Nothing applies until you say so
What comes back is a preview draft, with an explanation of what it intends to do and a count of the cells it would affect. You apply it or you discard it.
Preview-before-apply is the same trust boundary Qrly uses for schema authoring: the model proposes a structured change, a human reads it, and only then does anything happen to the document.
XLSX with real formulas — plus CSV, JSON, XML
XLSX export carries all sheets with real formulas and styles mapped to Excel, so the recipient gets a working model rather than a picture of one. CSV exports a single sheet as evaluated values, and JSON and XML are available for anything downstream that wants structure.
Formulas surviving the export is what makes the workbook forwardable. An accountant who receives values-only has received a screenshot with extra steps.
Same bytes, emailed or downloaded
Exports are rendered server-side. The file a scheduled subscription emails to a distribution list is byte-identical to the file a read-only viewer downloads from the report page.
That removes an entire class of argument — the one that starts with two people comparing the version they each received and discovering the numbers do not agree.
Common questions
Is this a real formula engine or a thin wrapper around a library?
It is Qrly's own tokenizer, parser and evaluator, running in the browser. It handles A1 references, ranges, whole columns, cross-sheet references and named ranges; lazy IF and IFERROR; cycle detection reported as #CIRC!; Excel number formats and date serials; workbook-aware functions such as ROW, INDIRECT and OFFSET; dynamic-array spilling; and the full Excel error set.
Roughly 500 worksheet functions are implemented, with Excel-correct overrides for UNIQUE, SUBTOTAL and AGGREGATE, GROUPBY, TEXTBEFORE and TEXTAFTER, WRAPCOLS, PERCENTOF and the REGEX family, within Excel's own limits of column XFD and 1,048,576 rows.
What happens to my own columns when the data refreshes?
They survive. Data blocks address source columns by name rather than by offset, so columns and rows you inserted inside a block are preserved and never written over. A source column that disappears is cleared in place rather than shifting everything left, new source columns append past the block, and key columns return each record to the row it was on before.
Stale cells that Qrly itself wrote are removed; untagged cells you wrote are never touched. Each refresh reports rows and columns written, columns and rows kept, columns dropped and rows skipped.
Where does the data in a spreadsheet report come from?
From one of four binding kinds, per block: QUESTION binds a saved question; VISUAL stores QQL that is recompiled on every run so it follows the connection's dialect; SQL is ad-hoc raw SQL authored in Monaco with schema completion; and AI takes a natural-language prompt, resolves it to SQL once, and refreshes as plain SQL thereafter.
Each binding sets its sheet, anchor cell, whether headers are included, a max row count and its key columns.
Does an AI-backed block spend tokens every time it refreshes?
Can I export to Excel with the formulas intact?
Yes. XLSX export carries all sheets with real formulas and styles mapped to Excel. CSV exports a single sheet with evaluated values, and JSON and XML are also available.
All formats are rendered server-side, so the bytes a subscription emails out are the same bytes a read-only viewer downloads.
Does the AI assistant edit my sheet directly?
Not without you seeing it first. The assistant works one-shot or as a multi-turn chat and can only propose operations from a whitelist — setCells, setRange, addSheet, format and clearRange — capped at 400 context cells and 200 operations per reply.
What comes back is a preview draft with an explanation and an affected-cell count, which you apply or discard.