Skip to content

Markdown

Tools for models

These turn results and errors into plain data for the result of a model's tool call, in the same shapes and value formats whatever the call. They work with any tool-calling framework, as json.dumps() always accepts what they return. Table.operation() makes the tools themselves.

Each example makes its own client from the environment, with bqtasks123 standing for the ID of the Job Tasks table. Job Tasks has Title, Status, Hours, Due Date, Code, which is unique, and Attachment. It starts with three records, and each example starts from those again. A live test runs every example with both clients and checks its output.

Results

For the full picture, see Using pyquickbase from an AI agent.

Return a result to a model

Describe a result for a model, as the result of its tool call.

page = await tasks.query_page(limit=50)
return tool_result(page, compact=True)

The result is {"ok": true, ...}, which json.dumps() always accepts, with a key for what it holds:

Value Keys
Page records, or columns and rows with compact, and page
A list of records or dictionaries The same. page.total is null, as it isn't known
Record or None record, which is null for None
WriteResult created_ids, updated_ids, and unchanged_ids
A list of SummaryRow groups, each with group, count, and its totals, and page
A summarize operation's result As for a list of SummaryRow, sorted and paged as asked
UpdatePreview changes, unchanged_ids, and missing_ids
A preview_update operation's result As for an UpdatePreview, with the approval keys
A preview_delete operation's result As for a Page, with the approval keys
SchemaPlan changes and has_changes, and the approval keys
SchemaChangeResult applied and already_done, and failed and not_attempted too
A list of TableSchema tables, each with fields and relationships
FormulaResult value and type
A count operation's result count
A delete operation's result deleted_count
Text, a number, a date, or a bool value
TableInfo, Field, or ReportInfo table, field, or report, without raw
A list of one of those tables, fields, or reports
FileDownload file, with file_name, content_type, size, and text or binary

A file's text is its contents if they're UTF-8 text, and otherwise it's null and binary is true, as a model can't read bytes.

page holds returned, total, skip, next_skip, and has_more, so a model can tell whether it has every record, and ask for the next page. An empty list counts as no records. For a summary, page counts groups. A query operation that left out some of the table's default fields, because it has more than its max_default_fields, adds fields_left_out with how many and a hint.

A preview's approval keys are blockers, what would stop the change, warnings, what else it would do, and, if nothing stops it, the confirmation the change needs and the UTC time it expires_at. A preview_delete operation's result has a blocker when more records match than the operation's max_records, as a delete with the same filter would be refused. Give both operations the same max_records for this to be right.

Values are written the same way whatever their field: a date as YYYY-MM-DD, a timestamp in UTC as 2026-03-04T05:06:07Z, a Decimal as a number when that's exact and as text otherwise, and every kind of empty value, such as "" or an empty file, as null. Records are keyed by label.

Parameters

  • value Any, required
    The result to describe.
  • compact bool, default False
    Send each label once, as columns, with a list of values for each record in rows, rather than repeating every label on every record. A file value becomes the latest file's name. Use it when a tool returns many records, to save tokens.
  • max_records int | None, optional
    The most records to include. Any more are left out, and page.has_more is true, so a model knows it doesn't have them all.
2 more: max_text_chars, max_chars
  • max_text_chars int | None, optional
    The most characters to include of each text value. A longer one is cut and ends by saying how much was left out, and text_truncated is true.
  • max_chars int | None, optional
    About the most characters the result may take as compact JSON, so that one call can't fill a model's context. Records and groups are included from the first until the next wouldn't fit, then page.has_more is true and page.next_skip says where to carry on, so paging still reaches every one. At least one is always included: if it's too large on its own, its text is cut shorter until it fits, down to 50 characters a value. Other keys, such as a file's text, aren't limited by it.

Returns dict[str, Any]

The result, as plain data.

Raises

  • TypeError: The value isn't one of these, such as an AppInfo, or holds a value of another type.
  • ValueError: max_records, max_text_chars, or max_chars isn't a positive whole number.
tool_result(
    value: Any,
    *,
    compact: bool = False,
    max_records: int | None = None,
    max_text_chars: int | None = None,
    max_chars: int | None = None,
) -> dict[str, Any]

Example

import json

from pyquickbase import Quickbase, Policy, tool_result

with Quickbase.from_env(
    policy=Policy.read_only()
) as qb:
    tasks = qb.app("bq1234567").table("bqtasks123")
    page = tasks.query_page(
        select=["Title", "Due Date"], limit=2
    )
    result = tool_result(page, compact=True)
    print(json.dumps(result, indent=2))
import json

from pyquickbase import AsyncQuickbase, Policy, tool_result

async with AsyncQuickbase.from_env(
    policy=Policy.read_only()
) as qb:
    tasks = qb.app("bq1234567").table("bqtasks123")
    page = await tasks.query_page(
        select=["Title", "Due Date"], limit=2
    )
    result = tool_result(page, compact=True)
    print(json.dumps(result, indent=2))

Output

{
  "ok": true,
  "columns": [
    "Record ID#",
    "Title",
    "Due Date"
  ],
  "rows": [
    [
      1,
      "Pour slab",
      "2027-01-15"
    ],
    [
      2,
      "Frame walls",
      "2027-02-01"
    ]
  ],
  "page": {
    "returned": 2,
    "total": 3,
    "skip": 0,
    "next_skip": 2,
    "has_more": true
  }
}

Errors

For the full picture, see Using pyquickbase from an AI agent.

Return an error to a model

Describe an exception for a model, as the result of its tool call.

try:
    page = await tasks.query_page(
        where=where, limit=50
    )
    return tool_result(page)
except QuickbaseError as error:
    return tool_error(error)

The result is {"ok": false, "error": {...}}, which json.dumps() always accepts. error holds code, message, hint, retryable, and data_changed, and more for some codes:

  • code is one of ToolErrorCode, which doesn't change between 1.x versions, unlike the wording of message.
  • hint says what the model should do next, in fixed words for each code. A failed schema change has its own, as it has no failed rows, and so does a partial_write whose data_changed is unknown, as its failed rows may have been written.
  • retryable is true when the same call can succeed later, such as after a rate limit or a timeout. retry_after is added when Quickbase said how long to wait.
  • data_changed is no if nothing was changed, partly if part of a write was, and unknown if a write may have been applied, such as after a timeout. A model should check the data before repeating a call whose data_changed isn't no.
  • A WriteError adds written, with the IDs that were created, updated, and left unchanged, and failed_rows, with each row's index, reason, field_id, and Quickbase's messages.
  • An APIError adds status, the HTTP status.
  • A TooManyRecordsError adds matched, how many records match, and limit, the most the summary may read.

An exception from outside the SDK gets the code internal and a message that says nothing about it, so a bug's details and any values in it don't reach the model. Log it yourself.

Parameters

  • error BaseException, required
    The exception, usually a QuickbaseError.

Returns dict[str, Any]

The error, as plain data.

tool_error(error: BaseException) -> dict[str, Any]

Example

from pyquickbase import (
    Quickbase,
    Policy,
    QuickbaseError,
    tool_error,
)

with Quickbase.from_env(
    policy=Policy.read_only()
) as qb:
    tasks = qb.app("bq1234567").table("bqtasks123")
    try:
        tasks.insert([{"Title": "Paint walls"}])
    except QuickbaseError as error:
        found = tool_error(error)["error"]
        print(found["code"], found["data_changed"])
        print(found["hint"])
from pyquickbase import (
    AsyncQuickbase,
    Policy,
    QuickbaseError,
    tool_error,
)

async with AsyncQuickbase.from_env(
    policy=Policy.read_only()
) as qb:
    tasks = qb.app("bq1234567").table("bqtasks123")
    try:
        await tasks.insert([{"Title": "Paint walls"}])
    except QuickbaseError as error:
        found = tool_error(error)["error"]
        print(found["code"], found["data_changed"])
        print(found["hint"])

Output

write_refused no
This tool may not make that change. Don't try it another way. Tell the user.

ToolErrorCode

The code in what tool_error returns. Codes don't change within 1.x, so a tool can act on them.

ToolErrorCode = Literal[
    "misconfigured",
    "needs_confirmation",
    "invalid_arguments",
    "too_many_records",
    "invalid_value",
    "read_refused",
    "write_refused",
    "unknown_table",
    "unknown_field",
    "unknown_report",
    "unsupported_report",
    "schema_mismatch",
    "partial_write",
    "write_failed",
    "write_permission_denied",
    "authentication",
    "permission_denied",
    "not_found",
    "bad_request",
    "rate_limited",
    "server_error",
    "api_error",
    "timeout",
    "network_error",
    "invalid_response",
    "quickbase_error",
    "internal",
]

Operations

For the full picture, see Using pyquickbase from an AI agent.

OperationName

What Table.operation() can offer a model, each running the table or report method of the same name.

OperationName = Literal[
    "query",
    "count",
    "summarize",
    "insert",
    "update",
    "delete",
    "preview_update",
    "preview_delete",
    "run_report",
    "run_summary_report",
    "run_formula",
    "download_file",
    "delete_file_version",
]

SchemaOperationName

What App.operation() can offer a model.

SchemaOperationName = Literal[
    "describe_schema",
    "preview_schema_changes",
    "apply_schema_changes",
]