Before you start
- These three tools never leave the cache. They read what Postern has already
synced.
fetch_liveis the one read that touches a source. get_schemais the contractqueryvalidates against. Call it first. It is filtered to your own grants, so it also tells you what you can reach.- A sector you were not granted is refused or filtered, depending on the tool. Why an agent was refused.
get_schema
The queryable objects and fields, per sector — the contractquery validates against.
string
A full object key (
finance.transaction) returns that object. A bare sector name
(finance) returns every object in it. Omitted, it returns everything the key is
granted.query, get_record, and fetch_live with an id. A sector with one object takes the
bare name.
This page does not repeat the field lists. Call get_schema: it is the authority, and
it is filtered to your own grants. Read notes and fieldDocs — they carry the sign
conventions, enums and unit traps that a field’s type and name cannot.
These rules hold across every object:
A sector the key was not granted comes back as an empty object, not a refusal.
get_schema filters to your grants after it resolves the name. So
get_schema("finance") on a key with no finance grant returns {}, which reads
like “this install has no finance data”. An unknown name is different and does fail,
with unknown sector: <name>. Read
describe_context before you conclude
anything from an empty schema.query
A structured read over one sector, served from the local cache. There are no cross-sector joins — fan out and correlate on your own side.string
required
The object key. A sector with more than one object needs the dotted form.
object
Field-keyed conditions. A bare scalar means equality; a bare
null means IS NULL.
Operators: eq, neq, gt, gte, lt, lte, in, contains.string[]
The projected columns. Omitted, it is every flat field. At most 50 entries.
integer
default:"100"
Capped at 500. In aggregate mode this caps groups instead of rows. Must be an integer
of 1 or more.
integer
Rows mode only, capped at 5000. Deep paging is not supported — narrow with
where.{ field: string, dir?: 'asc' | 'desc' }[]
Rows mode only.
dir defaults to asc. At most 50 entries, no field twice.{ fn, field?, groupBy? }
fn is one of count, sum, avg, min, max. count takes no field; the other
four require a numeric one. groupBy takes up to 50 flat fields.boolean
Finance objects only. See Two banks, one row, below.
count is what this response holds. It is never a total, and never the answer to
“how many are there” — that is aggregate: { fn: "count" }. has_more comes from a
probe row rather than from count === limit, so a page that fills exactly reads
has_more: false truthfully.
Groups come back ordered by the aggregate value descending, then by the group columns
ascending. count there is a JSON number. sum, avg, min and max are decimal
strings, or null when the aggregated set was empty. Postern refuses aggregate
alongside select, orderBy or offset, and the refusal names the field.
Rows come back ordered by the object’s primaryTime descending, with id ascending as
the tiebreak, unless you order them yourself. The order is deterministic within one
snapshot, but a later page can shift as a sync lands new rows.
Two clauses are on every query and are not yours to set: the read is scoped to the
owner’s own rows, and rows the source has deleted are excluded. Those rows stay
readable through get_record.
Operator detail that changes answers:
Project the untouched payload.
select may name a sector’s raw columns as well
as its flat fields: raw on every object, plus attendees on calendar and
attributes on home. That reads a row and its untouched detail in one call, instead
of a get_record follow-up. Projection only — those columns stay out of where,
orderBy, groupBy and aggregate, which validate against the flat fields alone. It
is opt-in because raw costs a fetch and a decompress per row.
Two banks, one row. When the same account arrives through both SimpleFIN and Plaid,
Postern serves one merged row and hides the copies. In the Console that bank’s row
carries also arrives via SimpleFIN — served as one. Both provider rows stay intact
underneath. includeShadowed: true reveals them, with read-only linkage columns
appended so a winner and its members can be tied together. Aggregates follow the same
rule, so that total double-counts a merged pair by design. On any sector but the three
finance objects the parameter is refused outright.
Four properties of finance data
Four more properties of finance data decide whether an answer is right. None is visible from a field’s type or name.What query refuses, by name
A sector you were not granted is a different case.
query and get_record are
refused before any database work, and the refusal names the sector rather than the
reason. Why an agent was refused.
get_record
One stored record by id — the full row behind the tidy view.string
required
The object key. A sector with more than one object needs the dotted form:
finance.transaction, not finance.string
required
Postern’s own id for the row — the
id field in a query result, not the provider’s
source_object_id.null.
This is the whole row as the database holds it. So it carries columns query never
projects, the untouched raw payload and the bookkeeping columns among them. It is
also the one read not filtered by deletion. A row with a non-null deleted_at
means the source has removed that object, and this is where you can still read it.
An id that is not a uuid returns null rather than failing, and so does a uuid naming
no row of the owner’s. null is the absent answer here, not an error.
A bare multi-object sector does fail, with get_record requires a full object key (finance.account or finance.transaction or finance.holding), got 'finance'.
Next
Freshness and live reads
How old the cache is, and the one read that leaves it.
MCP tools
The address, the transport, and what every tool call returns.