Changelog¶
All notable changes to this project are documented here. The format follows Keep a Changelog, and the project uses Semantic Versioning.
[Unreleased]¶
Fixed¶
Policy.bind()no longer fails for a role without "Edit app structure and permissions", which Quickbase doesn't show relationships to. If the policy hides a field or limits rows, every lookup and summary field is then hidden too, because what they read can't be traced.
[1.0.0rc13] - 2026-11-08¶
This candidate adds CSV and Excel files, DataFrames, more statistics in summaries, field usage, app
events, and creating, copying, and deleting apps. It removes the pyquickbase codegen command and
typed tables, and a file generated by an earlier release no longer imports, as listed below.
Added¶
- CSV and Excel files.
to_csv()andto_excel()onquery(), a report'srun(), and anyPagewrite records as they're fetched, with a header row.Table.rows_from_file()reads a CSV, tab-separated, or Excel file into aFileRows, converting each value by its field's type, such as1,234.5,45%,yes, or a user's email, and listing every value it can't convert with its row and column.insert(),update(),upsert(), andpreview_update()take it, and refuse it, writing nothing, while it has errors. CSV text that a spreadsheet would run as a formula is written with a leading', and Excel text is always text. Excel needspyquickbase[excel]. rows_from_file(max_rows=...)andmax_unzipped_bytes=...limit what a file may hold, for a file you didn't make. Reading stops atmax_rows, and an Excel file that would unpack to more thanmax_unzipped_bytes, 1 GB by default, isn't opened. Both raiseFileTooLargeError, anInvalidValueError, withlimitandunit.-
The async client works with files on a worker thread.
rows_from_file(),to_csv(), andto_excel()on a query or report no longer stop the event loop while they parse or save a file. -
More statistics in
summarize():median,std_dev(sample standard deviation), anddistinct_count, besidesum,avg,min, andmax. Each is exact, worked out fromDecimalvalues, and thesummarizeoperation offers them to models, with aliases such asstdev. summarize(compare_periods=True)gives each row achange, aPeriodChangewith how its count and statistics changed since the period before, within the same other groups. A period without records counts as empty rather than being skipped.-
SummaryRow.statistic()reads a statistic by name, andStatisticNamenames them. -
DataFrames.
to_pandas()andto_polars()onquery(), a report'srun(), and anyPagefetch records into a DataFrame, with a column for each field, which a query with no matches still has.insert(),update(),upsert(), andpreview_update()take a pandas or polars DataFrame as their rows, where a missing value leaves a field empty. Installpyquickbase[pandas]orpyquickbase[polars]. Neither library is loaded unless you use it. Table.field_usage()says where fields are used, such as in reports, forms, and pipelines, as aFieldUsagefor each, from the counts on each field's Usage page. A plan that deletes a field lists these uses in its warnings.App.events()andTable.events()list webhooks, Quickbase actions, notifications, subscriptions, reminders, and record-triggered automations, asAppEvents. Both previews of writes list the active ones on the table inevents, since changing records may set them off, andtool_result()includes them, with a warning, in a preview operation's result.Quickbase.create_app(),App.copy(), andApp.drop()create, copy, and delete apps.drop()needs the app's exact name. A client with a policy refuses all three, as a policy covers one app.-
Page.columns, the labels of the fields a page returned, in order. -
tool_result(max_chars=...)limits how many characters a result takes as compact JSON, so one call can't fill a model's context. Records and summary groups are included until the next wouldn't fit, andpage.has_moreandpage.next_skipsay where to carry on, so paging still reaches every one. At least one is always included, with its text cut shorter if it's too large on its own. - A
summarizeoperation sorts and pages its groups.order_bysorts them bycount, by agroupfield, or by asum,avg,min, ormax, with empty values last, andlimitandskippage through them. A question such as "which five clients have the most projects" no longer returns every client. The summary is still of every matching record. operation("query", max_default_fields=...)returns only the first of the table's default fields when the model doesn't name any inselect, andtool_result()addsfields_left_outwith how many it left out and a hint to name them.TooManyRecordsError, withmatchedandlimit, and the tool error codetoo_many_records.
Changed¶
anyio4.1 is now the lowest supported version, as worker threads need it under Trio.-
Writes refuse two values Quickbase loses without an error: text that isn't a plain number for a number field, such as
"1,234.5", which Quickbase leaves empty, and a user written as{"email": ...}, which it ignores. Both raiseInvalidValueError. Plain numbers as text, such as"0.45", still work. -
Every statistic is defined once, in
_summary.STATISTICS, which the summary, the operation's schema, group sorting, andtool_result()read, so adding one changes one place. -
preview_delete()returns aDeletePreview, aPagewithevents. -
ReadActionaddsfield_usageandapp_events, andWriteActionaddscreate_app,copy_app, anddelete_app. A custom rule that tested forotheron these requests no longer sees them. -
Summary groups have a
pageintool_result(), withreturned,total,skip,next_skip, andhas_more, as records do. Asummarizeoperation returns its page of groups, which reads as a list ofSummaryRow, as before. summarize()reads in large pages. It used to read 1,000 records per request. Nowpage_sizedefaults toNone, which sizes each page by the fields the summary reads, from 25,000 records for one field down to 1,000, so a page takes about 25 MB of memory. Totalling 250,000 records takes about 11 requests instead of 250. Passing apage_sizestill uses it.- A summary that only counts reads no records. With no
group_by,sum,avg,min, ormax,summarize()asks Quickbase how many records match, in one request, at any size. It used to read every matching record, with the table's default fields.max_records_scanned, the argument and the read rule's, no longer limits it, as it reads nothing. - The API reference is reorganized. Every page has each entry's code beside it and its contents
in the left sidebar, and entries you call are titled by what they do. The menu groups the pages,
and the overview lists every public name. "Limits and retries" is now three pages:
PolicyandBoundPolicyare under Policies, the rules under Rules, andRetryPolicyunder Retries, so links toreference/limits/need updating. - A summary over
max_records_scannedraisesTooManyRecordsError, andtool_error()gives it the codetoo_many_recordswithmatchedandlimit. It was aQueryErrorwith the codeinvalid_arguments.TooManyRecordsErroris aQueryError, soexcept QueryErrorstill catches it, but a tool that checks forinvalid_argumentsneeds to check fortoo_many_recordstoo.
Removed¶
- The
pyquickbase codegencommand, and thepyquickbaseconsole script that ran it. Useapp.table()with field labels, or keep classes you've already generated as your own code. - Typed tables:
App.typed(),TypedTable,TypedIterator, and their async versions, withTypedRecord,TypedField,TypedPage,Column,Row,Select,OrderBy,Unset, andUNSET.tool_result()no longer takes aTypedRecordorTypedPage. A file generated by an earlier release no longer imports.
Fixed¶
Policy.bind()is typed for the async client. Binding with anAsyncAppreturnedAny, so a type checker didn't see theBoundPolicyafterawait. It now returns a coroutine of one, andappis typedApp | AsyncApp.
[1.0.0rc12] - 2026-11-07¶
This candidate replaces the permission classes with one Policy, and adds schema changes that are
planned before they're made, for scripts and for agents. It breaks every client that used
read_only, read_policy, or write_policy, with no compatibility layer, as listed below.
Added¶
Policy, one argument for what a client may do.AsyncQuickbase(policy=...)andQuickbase(policy=...)take a policy ofallow,deny, andlimitrules over tables and fields, by name and label, or afieldsortablesselector. Anything no rule allows is refused, and a deny always wins.Policy.bind(app)looks up every name once, reports every problem in oneConfigurationError, and returns a frozenBoundPolicykeyed by ID.- One evaluator.
BoundPolicy.decide()is the only code that decides a request, and returns aDecisionwith the layer, the rule as written, a reason, and a hint, naming tables and fields by name and ID.allows()andexplain()answer questions about it, andoffers()asks it about each field and verb, which is what operations and schema operations offer, so a model is never offered what's refused. - Classes name groups of fields, and carry over to every formula, lookup, and summary that reads one, through chains and across tables, including the fields a summary's filter uses. A formula that can't be read counts as reading its whole table.
new_fieldssays what a field added after binding gets:read,hidden, or a class. It's never writable until the policy is bound again.limit.rows()ANDs a condition onto every query, summary, and delete the client sends, including into a new summary field's filter. It hides existing summaries that read past it, and lookups from a row-limited table, and refuses reports, downloads, and formulas there.- Schema verbs granted where a change lands:
lookupon the child, withreadon the parent's field,summaryon the parent, withreadon the child's field andfilteron its filter's fields, andlinkandunlinkon both tables. Creating a table gives the client a newBoundPolicy, asclient.policy, with the table in itscreatedlist fortables.created(). - Saving a bound policy with
to_json()andBoundPolicy.from_json(), and ahashof it. - Rules of your own, as objects with
decide(request), which can only refuse. PlannedChange.policy_effects, which says who could read and write a new field, its classes, and which rules lose a deleted field or table. A rule a deletion leaves matching nothing is a blocker, and so is a label that a field the client can't see already uses.- Summaries of some child records.
ensure_summary_field(),ensure_count_field(), andCreateSummaryFieldtakewhere, which is checked against the child's fields before it's sent, as Quickbase saves a filter it can't use and the summary then reads as empty. Under a policy, the filter may only use fields the client can filter on, and it's shown, and compared with an existing summary's, when every field it uses is readable on the child.Field.summary_queryholds a summary's filter, and a plan warns when a deleted field is one a summary filters on. App.preview_schema_changes()andApp.apply_schema_changes(). Changes such asCreateField,UpdateField,DeleteField,CreateTable,CreateRelationship,CreateLookupField, andCreateSummaryFieldare checked against the app as it is. EachPlannedChangesays what it would set, itsrisk, what else it would do or affect, such as records that lose a deleted field's values or formulas that use it, and anything that blocks it, including the client's policy. Applying plans again first, and refuses if the touched tables and fields have changed. A failure raisesSchemaChangeError, which says what was made, and planning again shows only what's left.- Schema operations for models:
App.operation()offersdescribe_schema,preview_schema_changes, andapply_schema_changes, which only runs with the confirmation its plan returned, for the same changes, while the schema is as it was. Each schema offers only what the client's policy allows. A change names what it does inaction, such ascreate_lookup_field, a relationship's tables inchild_tableandparent_table, and a summary'sfunctionin lower case, such assumorstd_dev.kind,child,parent,create_lookup, and Quickbase's own function names are read too. Each planned change isready,already_done, orblocked, and a plan's result has the same top-levelchangesand approval keys as an update preview. - Checks on what a new formula, lookup, or summary reads. Under a policy, one may only use fields the client can read, and a formula may only call functions known to stay on the record. A formula can't make a link or rich text.
Table.update_info()renames a table or changes its descriptions, andTable.delete_relationship()unlinks a child from its parent.confirmation_keyon the client, so several processes accept each other's previews.TableSchema, andtool_result()for plans, results, and table schemas.ConfirmationError, with the tool error codeneeds_confirmation, for a change without the confirmation its preview or plan returned, or with one that's expired, for other arguments, from another client, or from a client with a different policy.- Operations and
where_from_dict()read the spellings models tend to write for a closed value, as the one value each means:starts withorstartsWithforstarts_with,eq,=,ne,<>,lt,gteand the like for the symbols,andandorforallandany,sortfororder_by, and anorder_byitem as a bare label, a single item, or withorder,dir, ordescending. Labels must still match exactly. A preview and its change share a confirmation whichever spelling each used. F.has()andF.not_has(), for one exact value of a multi-select or list-user field.
Changed¶
- A
partial_writewhosedata_changedisunknownhas its ownhint: check what was written, then send only what's missing, as the batch that failed may have been written. Layerno longer listsconfirmation, which no check made. A missing or mismatched confirmation raisesConfirmationError, notPolicyRefusedError.- A failed schema change has its own
hintintool_error(): preview the same changes again, which shows only what's left, as it has no failed rows to fix. - Each limit is named for what it limits.
Table.operation()takesmax_records_per_callandmax_records_per_writeformax_recordsandmax_rows.summarize()takesmax_records_scanned.allow.read()takesmax_paging_depthandmax_records_scannedformax_recordsandmax_summary_records, andallow.delete()takesmax_records_per_delete, as do the fields ofOffers.tool_result()takesmax_text_charsformax_text, and a delete preview'smostismax_deletable. - Descriptions say what they describe.
Table.operation()andwhere_schema()takefield_descriptions, andTable.operation()takesreport_descriptions={ID: text}, withreportsholding only IDs. create_field()andensure_field()taketype, notfield_type, asField.typeand the schema changes do, and so does a generated class'sColumn.create_relationship()andensure_relationship()takeforeign_key, notforeign_key_label.FileUploadandFileDownloadholdfile_name, notname, asFileVersionand the JSON file value do, and a downloaded file intool_result()hasfile_name.record_url_pattern()isrecord_url_template(), with{record_id}where the ID goes.- Paging has one word per meaning.
limitis the most records one call returns, everywhere: it replacestopinquery_page(),preview_delete(),Report.run_page(), their typed versions, andReadRequest.max_totalreplaceslimitonquery(),Report.run(), andsummary_rows(), where it stops iterating across pages. - Writes take
records, notrows, ininsert(),update(),upsert(), andpreview_update(), as the operations do. - Aggregates are
sum,avg,min, andmaxinsummarize(), onSummaryRow, in thesummarizeoperation's arguments, and intool_result()'s groups, as summary fields are. The operation still readstotal,average,minimum, andmaximum. - Results and errors say what they hold. A
countoperation's result is{"count": n}and adeleteoperation's is{"deleted_count": n}, rather thanvalue. An error'schangedisdata_changed, withno,partly, orunknownfornone,some, orunknown. A shortened text value setstext_truncatedrather thantext_shortened, and a failed row'skindisreason. - Error codes:
invalid_queryisinvalid_arguments, as it covers every operation's arguments,write_permissioniswrite_permission_denied,configurationismisconfigured,networkisnetwork_error, anderrorisquickbase_error. RowError.kindisRowError.reason.- The
summary_reportoperation isrun_summary_report, to matchrun_report. ensure_summary_field()takesfunctionin lower case, such assumorstd_dev, as schema changes do. Quickbase's names, such asSUM, still work, and an unknown one raisesInvalidValueErrorbefore anything is sent.describe_schemanames each relationship's parent table inparent_table, beside its ID.order_byitems take"direction": "asc"or"desc", defaulting toasc, in place of"descending": true, which is still read.-
The filter operators
HASandXHASarehasandnot_hasinwhere_from_dict()and the schemas. The capitals are still read. -
Every refusal is a
PolicyRefusedError, holding itsDecision, with a message of the formRefused by <layer>: <what>. <reason>. (rule: <rule>) Nothing was sent. To allow it: <hint>.A hidden field still raisesUnknownFieldError.tool_error()keeps the codesread_refusedandwrite_refused, chosen by the verb refused, and dropsrefused. - A confirmation starts with an ID for its key, and is bound to the hash of the client's policy.
ConfirmationErroris no longer aQueryError.- A write whose field keys can't be read is refused, even where every field may be written.
WriteRequest.actionnames each schema change, such ascreate_field, rather thanother, andWriteRequesthasapp_id,parent_table_id,field_type, andderived.- Previews share their approval keys:
blockers,warnings,confirmation, andexpires_at. A delete preview over its limit lists a blocker, rather thanover_limit.
Removed¶
read_only,read_policy, andwrite_policy. Passpolicy=Policy.read_only()forread_only=True, and a boundPolicyfor the others.ReadRules,ReadAccess,WriteRules,WriteAccess,SchemaAccess, andall_of(), with theirnew_tablesarguments, and theReadPolicyandWritePolicytypes. A policy function becomes aRuleof your own, which can only refuse.WriteRefusedError,ReadRefusedError, andRequestRefusedError, forPolicyRefusedError.
[1.0.0rc11] - 2026-11-05¶
This candidate adds links to the Quickbase interface, so that a tool built on the SDK can show a person where to check what a model read or changed. From testing an MCP server in Claude Desktop, an operation can now require its preview, and describe its scope to the model. Nothing that worked before has changed.
Added¶
- Links to the Quickbase interface, so a tool can show a person where to check what a model read
or changed.
App.url(),Table.url(), andReport.url()link to an app, table, or report,Table.record_url()to a record's page for viewing or editing it, andTable.field_url()to the page that edits a field.Table.record_url_pattern()gives one link with{Record ID#}where each record's ID goes, so a tool needn't send a link per record. Names are looked up through the client's cache, nothing else is sent to Quickbase, and anything the client'sReadRuleshide is refused as unknown.App.url()is a plain method in both clients, as it needs no lookup. - Links to the schema pages a model's changes send a person to:
App.settings_url(),Report.url(edit=True)for the report's settings, and onTable,new_record_url(),fields_url(),relationships_url(),settings_url(), andform_url(form_id), which takes the form's ID because the API can't list forms. require_preview=Trueon theupdateanddeleteoperations. In testing, a model updated a record with no preview, though told to preview first. With this, they only run with theconfirmationtheir preview returned for exactly the same arguments, within 15 minutes, and a delete is refused if how many records match has changed since.tool_result()includes the confirmation. Confirmations are signed with a key each client makes, so nothing is stored.scope=ontable.operation(), as well as onrun(). In testing, a model that couldn't see a scope tried to set its field, was refused, and then told the person something untrue about why. Given tooperation(), the scope is described in the schema, such asBillable is true, its fields are left out of a write's schema, a write that sets one is refused with a message saying why, and aninsertscope that couldn't work is refused when the operation is made.Table.compile_where()returns a condition, fromF,where_from_dict(), or a string, as the query text that a query of the table would send.
[1.0.0rc10] - 2026-11-05¶
This candidate helps a model get its arguments right, from testing an MCP server built on the SDK in Claude Desktop: report operations take a report the way models send one, schemas say what a percent means, and refusals for a label or choice in the wrong case give the right one. Every call that worked before still works, and only some refusal messages read differently.
Fixed¶
- The
run_reportandsummary_reportoperations refused a report sent any way but its ID as text, so a model that sent8rather than"8", or the report's name, was told it couldn't run a report its tool listed.run()now also takes the ID as a number, or the report's exact name, ignoring case, of a report the tool lists. A name shared by two of them is refused, asking for the ID. The schema still lists the IDs, and its description says a name works too. - Nothing told a model that a percent is a fraction, so asked for "25% done" it could write
25, which Quickbase stores as 2,500%, or filter on50, which matches nothing. An operation's schema now says so for a percent field it writes, andwhere_schema(), givenFieldobjects, says what a number means on a percent or duration field it offers. - A field label or choice in the wrong case gets a refusal that names the right one, such as
select[0] isn't a field this tool may use there. Labels must match exactly, as in 'Title'.It only names a field or choice the tool offers, and only when exactly one matches. - A report of the other kind gets a refusal that says what to do. A table report sent to
summary_report, or a chart sent torun_report, says which operation runs it, where the message only said it couldn't be run.
[1.0.0rc9] - 2026-11-05¶
This candidate fixes filters on percent and duration fields, which matched the wrong records with no error, and helps a model's tools say why a delete or write would be refused before anyone approves it. Each was found by testing an MCP server built on the SDK, and checked against the live API.
Upgrading. A percent or duration filter that worked around the bug by sending Quickbase's own
units as a number, such as F("Done") >= 50 for 50%, now means 5,000%. Send 0.5, or the text
"50", which is still sent as written.
Fixed¶
- Filters on
percentanddurationfields compared the wrong values, and matched the wrong records with no error. A query compares a percent in whole percent, soF("Done") >= 0.5matched 0.5% and up. A bare number on a duration matches nonsense, soF("Budget") > 28800000matched nothing.Fandwhere_from_dict()now send a percent as whole percent and a duration asN milliseconds, which compares exactly, so a number means what it means when writing: a fraction, and milliseconds. Text is still sent as written. The same applies to ascopeand todelete().
Added¶
Ftakes atimedeltaon adurationfield.- A
preview_deleteoperation's result says, intool_result()'sover_limit, when more records match than itsmax_records, so a model knows the delete would be refused before asking anyone to approve it. - A write that sets a field the tool may read but not write now names the field. A field it can't read still isn't named.
Changed¶
- The refusal for a read past
ReadAccess(max_records=...)says that it limits how far paging can go, not only the size of a page, and so do the docs forReadAccessandtable.operation().
[1.0.0rc8] - 2026-11-05¶
This candidate holds the fixes from a review before 1.0. Most important, it closes gaps in what a model's
tools could do: an operation's scope limited what a write filtered but not what it wrote, and the
delete operation had no limit. It also reads user and file values as dataclasses, adds select to
typed tables, retries reads after more kinds of failure, and fills in what the API was missing, such as
COUNT summaries, duration writes, and field settings. Each was checked against the live API.
Upgrading. These changes can break working code:
- User and file values are dataclasses, not dictionaries. Read
owner.emailrather thanowner["email"], andattachment.latest.file_namerather thanattachment["versions"][-1]["fileName"]. An empty file field is None, where it was{"url": "", "versions": []}. - A
durationfield takes whole milliseconds or atimedelta. A fraction, such as1.5, or text, such as"1:30:00", raisesInvalidValueError. - An operation's write can't set a field its scope uses, and
insertneeds a scope of==conditions joined with&. - httpx 0.28 or later is required.
Security¶
- An operation's
scopenow holds for what a write sets, not only for what it filters. Before, a model with aninserttool scoped to one project could create records in another, and anupdatetool could move a record out of its scope.insert,update, andpreview_updatenow refuse a row that sets a field the scope uses, andinsertfills those fields in from the scope. It refuses to run when the scope isn't only==conditions joined with&, as a new record couldn't be made to match it. - The
deleteoperation counts what its filter matches first, and refuses the call, deleting nothing, when that's more thanmax_records, 100 by default. Before, one call could delete every record in its scope. The tool's schema tells the model the limit.
Added¶
Typed tables and values:
User,FileAttachment, andFileVersion, the frozen dataclasses that user,multiuser, and file fields read as, in records and generated classes. AUser'sreprleaves out the email address, and aUsercan be written to a user field or compared inF.selecton a typed table'squery(),query_page(),first(), andget(), which reads only the fields of the class you list. The other attributes are left unset, so reading one raisesAttributeErrorsaying it wasn't selected.Selectis its type.- Generated classes type
vCardButtonandICalendarButtonfields as read-onlystr, andpredecessorfields as a writablestr, rather thanAny. Runpyquickbase codegenagain to get them.
Writing:
WriteResult.row_ids, which gives each row of aninsert()its new Record ID#, in the order of your rows, with None for a row that failed.created_idsleaves failed rows out, so it only lines up with your rows when none failed.- Operations can write
durationfields, as whole milliseconds, andpredecessorfields, as text. Table writes and generated classes also take atimedeltafor a duration. timeoutoninsert(),update(),upsert(), anddownload_file(), and on a typed table's writes, which replaces the client's timeout for that call, such as for a large file. It applies with your ownhttp_clienttoo.
Schema:
ensure_count_field(), which counts a child table's records on the parent, and counts 0 for a parent with none.COUNTtakes no field, soensure_summary_field()now raisesInvalidValueErrorfor it, where Quickbase refused every field it was sent.help_text,bold,no_wrap,find_enabled, andappears_by_defaultoncreate_field(),update_field(), andensure_field(), andadd_to_formsoncreate_field(), for the settings Quickbase takes besideproperties.Field.help_textreads a field's help, andReadRuleskeep it.
Changed¶
- Breaking: user and file values are no longer dictionaries, as described under Upgrading.
tool_result()writes a user as{"id", "name", "email"}, and a file version as{"number", "file_name", "uploaded", "creator"}. - Breaking: a
durationfield refuses fractions and text, because Quickbase drops a fraction of a millisecond without saying so, and guesses at text. - Breaking: pyquickbase needs httpx 0.28 or later, whose request bodies it measures to keep each batch under Quickbase's 40 MiB limit. httpx 0.27 sent larger bodies than were measured.
- Reads, meaning GET requests, record queries, report runs, and formula runs, are retried after a
timeout, a dropped connection, or a 502, 503, or 504, as sending one again changes nothing. A write
still isn't. Any request is also retried after httpx's
PoolTimeout, as nothing was sent. - The
preview_updateoperation offers only fields the client may both read and change, so it no longer offers a write-only field that every call would be refused for.
Fixed¶
summarize()on a client withReadRulesraisesReadRefusedErrorif more records match while it reads thanmax_summary_records, rather than returning a summary of only some of them.tool_error()reportschanged: "some"when a failed call still changed something: whendelete_fields()deleted some of the fields, and whencreate_field()created the field but couldn't setrequiredorunique. That error's message now says the field was created.insert(),update(), andupsert()raiseInvalidValueErrorfor a NaN or infinite number, rather than aValueErrorfrom httpx.Decimalvalues, in writes and queries, andfloatvalues in queries, are written without an exponent, soDecimal("1E+3")is sent as1000. A NaN, infinite, or enormous number in a query raisesQueryError.preview_update()reads in batches no larger than the client'sReadRulesmax_records, so a preview of more records than that is no longer refused.UnknownReportErrorsays the report may be hidden from the token's role, as Quickbase hides one whose filter or sort uses a field the role can't see. Running such a report by ID throughqb.request()givesInternal error, and that error now says why.
Documentation¶
- The guides to errors, files, and formulas are rewritten by task, and the codegen and clients guides gain complete first examples. Each ends with a table of what each refusal raises and leaves behind.
- The playground simulates updates, deletes, and queries on user fields, with two new examples, and each guide section links to the example that shows it.
- The API notes record how Quickbase writes durations, counts child records, and takes field settings.
[1.0.0rc7] - 2026-11-05¶
This candidate adds limits on what a client can read, output for a model's tool call, and table
operations offered to a model with their JSON Schema. It also makes the API reference a set of pages. It
has breaking changes: the write limits are renamed and allowed_writes is removed, so check the list
under Changed before upgrading.
Added¶
read_policyonQuickbaseandAsyncQuickbase, which decides on each read before it's sent, and works with any of the limits on writes.ReadRulesandReadAccesslist what a client may read from each table: which fields it may return, filter on, and sort by, which reports and file fields it may use, and how many records one query can reach. A table or field they don't list acts as if the token couldn't see it. They also leave email addresses out of user and file values unless asked, and keep only the metadata the SDK reads, so a field's formula and a report's filter aren't inraw. They refuse formulas, app details and roles, reports run throughrequest(), and any change to tables or fields. A policy can also be your own function, which takes aReadRequest.ReadRefusedError, raised for a read the policy refuses, andRequestRefusedError, the base of it and ofWriteRefusedError.tool_result()andtool_error(), which turn results and errors into plain data for the result of a model's tool call, for any tool-calling framework. Results use the same shapes and value formats every time: dates asYYYY-MM-DD, timestamps in UTC, every empty value as null, and paging asreturned,total,skip,next_skip, andhas_more.compact=Truesends each label once, andmax_recordsandmax_textcut a result while saying so. Errors carry a fixedcodefromToolErrorCode, ahintfor the model, whether a retry can help, and whether anything waschanged, which tells a partial write from a complete failure. An exception from outside the SDK says nothing about itself.table.operation(), which turns one table operation into a tool for a model: anOperationwith the JSON Schema of the arguments a model may send, andrun(), which checks them against the same lists before calling the table. It covers queries, counts, summaries, inserts, updates, deletes, their previews, table and summary reports, formulas, and file downloads and deletes. The schema offers only what the client'sReadRulesandWriteRulesallow, and an operation the client can't run is refused when it's made.run()turns JSON values into dates, times, numbers, and file uploads by field type, andscopeadds a condition your code fixes to every call. A formula's text is always fixed by your code.descriptionstells the model what fields mean.tool_result()takes table, field, and report metadata, withoutraw, and a downloaded file, with its text when it's UTF-8 text.where_schema()takesdescriptions, andwhere_from_dict()takesFieldobjects infields, so the same list works for both.
Changed¶
- Breaking: renamed
TableRulestoWriteRules,AllowtoWriteAccess, andReadOnlyErrortoWriteRefusedError, so that the names for limiting writes match those for limiting reads. A refused write still raises the same error in the same places. - Breaking: removed
allowed_writes, which only did what awrite_policycan. A policy that checksrequest.methodandrequest.pathdoes the same, as Combining limits shows. all_of()combines read policies as well as write policies, but not the two kinds at once.-
A field or table that can't be found now says "this client can't see it" rather than "this token", as a read policy can hide it too.
-
The message of
AuthenticationErrornow says that a 401 may mean a wrong ID rather than a bad token. Quickbase uses the same wording for a bad token, an app or table that doesn't exist, and one the token can't reach, so the error's type stays the same.
Documentation¶
- The API reference is now a set of pages, one for each class or topic, with a description of every
argument and attribute and an example for both clients, which live tests run. Each page has a plain
Markdown copy, listed in
llms.txt. - Links to anchors in the old reference page, such as
reference/#pyquickbase.Table, no longer work. - The reference page "Write limits and retries" is now "Limits and retries", and covers read limits.
[1.0.0rc6] - 2026-11-05¶
This candidate adds what an AI agent or MCP server needs to use Quickbase safely: filters from plain
data, limits on what a client can write, previews of writes, and summaries. It has one breaking change,
to insert(), so check the list under Changed before upgrading.
Added¶
where_from_dict(), which builds a query condition from plain data, such as the JSON from an AI model's tool call or a web form. It refuses anything it doesn't recognize, and checks each value against its field's type, sotodayorgarbageon a date field raisesQueryError. Itsfieldsargument limits which fields a filter may use.where_schema(), which describes that format as JSON Schema with no$ref, for a tool's input in MCP or any other LLM tool system.write_policyonQuickbaseandAsyncQuickbase, which decides on each write before it's sent.TableRulesandAllowlist what a client may do to each table, by action and by field, and refuse everything else. A policy can also be your own function, which takes aWriteRequest, andall_of()requires several to pass. Only a return value ofTruelets a write through.preview_delete()andpreview_update()on tables and typed tables, which show what a delete or update would do without changing anything, so that a person or model can check it first.preview_update()returns the newUpdatePreview, with each change's before and after values.summarize()on tables and typed tables, which counts records and finds totals, averages, minimums, and maximums, overall or per group, withPeriodto group dates by month and so on. It reads every matching record, andmax_recordsrefuses a scan larger than you expect, rather than returning a partial result.Report.summary_rows(), which reads a summary or chart report built in the UI as{column label: value}rows. These reports used to needqb.request().
Changed¶
- Breaking:
insert()now raisesInvalidValueError, and sends nothing, on a table whose key field isn'tRecord ID#. Before, a row with an existing value in that field silently changed that record rather than creating one, andcreated_idscould end up shorter than the rows. Useupsert()on those tables. Changing the key field in the UI can take as long asschema_ttlto be noticed.
[1.0.0rc5] - 2026-11-04¶
This candidate closes gaps that would be hard to change after 1.0, and has four more breaking changes, so check the list under Changed before upgrading.
Added¶
Expression,FieldRef,FieldRefs, andOrderByare now exported, as they appear in public signatures.batch_sizeoninsert(),update(), andupsert()of a typed table.TypedTable.query_page(), which returns a page of generated class instances as a newTypedPage, withtotal,skip,has_more, andnext_skip. It runs the same schema check as other typed methods.
Fixed¶
- A
datetimewith a fraction of a second can now be written, includingdatetime.now(UTC). Before, Quickbase refused it as a row error, because Python's default of six fractional digits isn't accepted. order_byandselecton an untyped table now accept a field from a generated class orF, such astasks.table.query_page(order_by=Tasks.fields.title). Before, it raised a bareTypeError. Any other unusable value now raisesQueryError.ensure_table()now returns a table you can use when the name differs only in case from an existing one. Before, it found the table, then raisedUnknownTableErroron first use.
Changed¶
- Breaking:
==and!=on auserfield now work, by email or user ID. Before,==silently matched nothing, and!=silently matched every record, including that user's, so adelete()with it could remove more than intended. Check any code that compares auserfield. - Breaking:
Fnow raisesQueryErrorfor_curuser_on auserormultiuserfield, because Quickbase reads it as the token's owner, so a value from someone else could match that person's records. A query string can still use it on purpose. - Breaking: a typed table now raises
InvalidValueErrorfor a row made by a class for another table, whose field IDs would mean different fields. Before, only a type checker refused it. - Breaking:
order_byon a typed table now takes only what its signature says, a field of the generated class or its ID, alone or in a list. Anything else raisesQueryError. Before,["Title"]and[F("Title")]happened to work,"Title"was read as one field name per letter, andF("Title")raised a bareTypeError. UseTasks.fields.title. - Fuller docstrings for the schema, query, and write methods, longer schema, reading, and writing guides, and a new field types page.
SchemaMismatchError's description now covers theensure_methods, which also raise it.
[1.0.0rc4] - 2026-11-04¶
These changes come from building a customer portal on the SDK.
Changed¶
- Breaking: generated classes now read
currencyfields asDecimal, and foreign keys andnumericfields set to 0 decimal places asint. A fractional value in anintfield raisesInvalidResponseErrorrather than being rounded, because Quickbase only uses that setting for display. Generate your classes again, and update code that expectsfloatfor these fields. - Breaking:
pyquickbase codegennow leaves out the links Quickbase makes for each relationship, such asAdd TaskandTask records. Pass--include-linksto keep them. - Breaking: a write refused by the token's role now always raises
WritePermissionError, which is both aWriteErrorand aPermissionDeniedError. Before, the same refusal raised one or the other depending on the role's other permissions.
Added¶
RowError.kindandRowError.field_id, so you can recognize a duplicate value, a refused row, or a row refused by a custom data rule, without reading Quickbase's wording.App.ensure_table(),Table.ensure_field(),Table.ensure_relationship(),Table.ensure_lookup_field(), andTable.ensure_summary_field(), for setup scripts that are safe to run again. The last two also add lookups to an existing relationship and create summary fields, which neededrequest()before.first()andget()onTableandTypedTable.App.roles(), and the newRoletype.Fieldattributes for what a lookup or summary points at, plusdecimal_placesandis_foreign_key.Relationship.summary_field_ids.from_env(token_env=...)andpyquickbase codegen --token-env, for a program with several tokens.
[1.0.0rc3] - 2026-11-02¶
Added¶
- A guide to using pyquickbase from an AI agent.
llms.txtandllms-full.txton the docs site, which hold a list of the docs and all of them in one file.- The playground's sample app now has three linked tables, with users, tags, attachments, a lookup, a formula, and rollups, and four more examples.
Fixed¶
pyquickbase codegennow writes the same line endings on every system, so generating on Windows doesn't change every line of a file generated elsewhere.- A
Retry-Afterof 0 now waits usingRetryPolicy's backoff instead of retrying immediately. Quickbase can send it while still limiting, so immediate retries could be used up during a burst. - If a later batch of a write got a successful response in the wrong shape,
InvalidResponseErrorlost the results of the earlier batches. It's now aWriteError, like any other failure in a later batch.
[1.0.0rc2] - 2026-11-02¶
Security¶
Fno longer lets a value act as query syntax, which could match, or delete, records it shouldn't._FID_6compares with field 6, soF("Title") == "_FID_6"matched every record, and it now raisesQueryError.a OR bmatchedaandbrather than that text, and now matches that text.
Fixed¶
insert()andupsert()now raiseInvalidValueErrorfor a row with no values, because Quickbase can silently skip it, which also shiftscreated_idsagainst your rows.pyquickbase codegennow always reads and writes UTF-8, so labels like日本語no longer fail or produce a broken file on Windows.table.relationships()documents that it lists only relationships where the table is the child.- A corrupt compressed response, or a redirect loop with your own
http_client, now raisesNetworkErrorinstead of anhttpxexception. - Reading pages now raises
InvalidResponseErrorif a response ignoresskip, instead of returning the same records forever.
Added¶
- A playground on the docs site, which runs the SDK in the browser against simulated data, with nothing to install.
[1.0.0rc1] - 2026-11-02¶
Added¶
- A stability and versioning policy, which applies from 1.0.
Changed¶
TypedTable.check_schema()now always fetches the table's fields again, instead of doing nothing after the first check. Other methods still check once, on first use, against cached metadata.- A generated class no longer fails when a field becomes writable. Otherwise, supporting a new field type in a later version would break classes generated earlier.
- Generated names no longer depend on the Python version running
pyquickbase codegen, so a label can't get a different name on a newer Python. - The error from
TypedRecord.from_record()now mentions that a report may have left out a field. SchemaMismatchErrorandUnknownFieldErrornow say a field may be hidden from the token, because Quickbase leaves out fields a role can't see, as if they didn't exist.
Removed¶
from_api()onAppInfo,TableInfo,Field,Relationship,ReportInfo,FormulaResult, andPage,FileDownload.from_response(),FileUpload.to_api(), andWriteResult.merged()andsummary(). These were internal helpers, and they're now private so they can change freely.
[0.2.0a1] - 2026-11-02¶
Added¶
- A documentation site.
- Reports:
table.reports(),table.report(), andReportwithrun(),run_page(), andinfo(), plusReportInfo,UnknownReportError, andUnsupportedReportError. Only table reports can be read as records. - File attachments:
FileUploadfor writes, plustable.download_file()andtable.delete_file_version(). pyquickbase codegen, which generates a typed class for each table, andapp.typed(), which reads and writes through those classes, includingquery(),insert(),update(), andupsert(). A class that no longer matches its table raisesSchemaMismatchError.- Formulas:
table.run_formula(), which returns aFormulaResult,formula=oncreate_field()andupdate_field(), andField.formula.
Changed¶
read_onlyandallowed_writesclients now allow running formulas, withPOST /formula/run.- A blank formula now raises
InvalidValueErroroncreate_field()andupdate_field(), including throughproperties, because Quickbase accepts it on a formula field and empties the field's values. - A file attachment field now accepts only a
FileUpload, and raisesInvalidValueErrorfor anything else, includingNoneand hand-built dictionaries, because Quickbase silently accepts those and keeps or loses the file. - Writes are also split by size, to stay under Quickbase's 40 MiB request limit, so
batch_sizeis now an upper limit. read_onlyandallowed_writesclients now allow running reports, withPOST /reports/{id}/run. This adds to the report's usage count in Quickbase, but changes no records.Record.idnow explains what's missing when a report's records have no Record ID#. It still raisesKeyError.
Fixed¶
- A rate limit error now includes Quickbase's message, which arrives in a different format.
- A README example that was missing an import.
[0.1.0a1] - 2026-11-02¶
First alpha release.
Added¶
QuickbaseandAsyncQuickbaseclients withrequest()for calling any endpoint.RetryPolicy, with retries for HTTP 429 and connection failures.- The
QuickbaseErrorexception hierarchy. qb.app()andapp.table(), with app, table, field, and relationship metadata and a schema cache.Ffor building queries by field label, with values escaped for you.- Checks that reject broken
wherestrings that Quickbase would otherwise accept. table.query()for automatic paging, plusquery_page()andcount().Record, with access by label or ID and date and timestamp conversion.read_only=Trueon the clients, which refuses any request that could change data.allowed_writeson the clients, which limits them to reads plus the writes you list.parse_where()for inspecting a hand-writtenwherestring.table.insert(),update(),upsert(), anddelete(), with per-row error reporting inWriteError.app.create_table(),table.create_field(),update_field(),delete_fields(),create_relationship(), anddrop().