Skip to main content

  1. Agentycs
  2. Developers
  3. APIs and protocols

One engine. Many front doors.

Your data is reachable through the protocols your tools already speak, and your applications are reachable over REST, a realtime socket and the Model Context Protocol at the same time.

What this page covers #

PostgreSQL wire protocol

Query the lakehouse with any Postgres client or BI tool, as if it were Postgres.

  • Standard drivers and existing dashboards connect directly
  • Analytics run over the live lakehouse, with no warehouse to load

Arrow Flight SQL

Columnar, high-throughput analytical access for data tools.

  • Results stream as Arrow record batches
  • Built for dataframe libraries and analytical engines

Graph queries

Traverse tabular data as a connected graph, without maintaining a graph database.

  • ISO GQL, Bolt-compatible clients
  • Relationships inferred from the live schema

REST, WebSocket and MCP

Every application exposed identically over all three, with live updates.

  • Generated REST facade over every query and mutation
  • Live queries: snapshot, then diffs, as data changes

Query the lakehouse #

Documents, structured records and synced external systems all resolve as tables. Analytics run against the live lakehouse: fresh writes are folded in transparently, so a query never returns a stale answer while it waits for a background flush.

PostgreSQL wire protocol
psql, JDBC and ODBC drivers, notebooks and dashboards connect as if to Postgres.
Arrow Flight SQL
Columnar, high-throughput access. Results stream as Arrow record batches.
Graph queries
Read-only ISO GQL, Bolt-compatible traversal over the same tables.
Natural language
A planner turns a plain-language question into permission-aware SQL, with citations back to the rows it used.
Query the lakehouse with psql · SQL
$ psql "postgresql://[email protected]:5432/operations"

=> SELECT site,
          count(*) AS open_findings
     FROM inspections
     JOIN findings USING (inspection_id)
    WHERE findings.status = 'open'
    GROUP BY site
    ORDER BY open_findings DESC
    LIMIT 20;

Documents, structured records and synced external systems are all queryable as tables. Your dashboard tool connects with the same string.

Join lakehouse data to a database you did not move · SQL
-- 'contracts' is a governed lakehouse table.
-- 'erp' is a Postgres database nobody migrated, registered as a source;
-- its tables resolve as an ordinary three-part reference.
SELECT c.customer_id,
       c.signed_at,
       a.owner
  FROM contracts AS c
  JOIN "erp"."public"."accounts" AS a USING (customer_id)
 WHERE c.signed_at >= now() - interval '90 days'
 ORDER BY c.signed_at DESC;

One planner, two systems. The predicate is pushed down to the external database in its own SQL dialect, the external side stays read-only, and the row filters and column masks that apply to the lakehouse table apply to the whole query.

A driver connection is not a way around governance. The filters run inside the query, not in front of it.

Call an application #

An application declares its operations once. From that declaration the platform generates a REST endpoint, a realtime target and an MCP tool, all in auth parity, plus the typed client that calls them.

Call an application operation over REST · Shell
curl https://agentycs.example.com/api/apps/inspections/open_findings \
  -H "Authorization: Bearer $AGENTYCS_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"site_id": "FAW-014"}'

Authenticated application traffic lives under /api/apps/{app}/. Public routes an app chooses to expose live under /api/public/apps/{app}/. The operation name is the one the application declared.

Subscribe to the same operation over server-sent events · Shell
curl -N https://agentycs.example.com/api/apps/inspections/open_findings \
  -H "Authorization: Bearer $AGENTYCS_API_TOKEN" \
  -H "Accept: text/event-stream"

event: snapshot
data: {"rows": [{"site": "Fawley", "open_findings": 12}]}

event: diff
data: {"changed": [{"site": "Fawley", "open_findings": 11}]}

One operation, two reading modes. Ask for an event stream and you get the current answer once, then only what changes. The same subscription is available over the realtime socket.

Open a realtime session from the browser · JavaScript
const socket = new WebSocket("wss://agentycs.example.com/ws");

socket.addEventListener("open", () => {
  socket.send(JSON.stringify({
    type: "query",
    target: "inspections.open_findings",
    params: { site_id: "FAW-014" },
  }));
});

Realtime targets are app-scoped, written as app.target. Browser sessions authenticate from the HttpOnly session cookie, never from a token in the URL.

Large answers and live answers #

Analytical results stream as record batches under a strict terminal contract, so an arbitrarily large answer arrives incrementally rather than being buffered whole. Application subscriptions work the other way round: you receive the current answer once, then only the diffs, as the underlying data changes.

  • Streaming results with an explicit terminal state, never a silent truncation
  • Live queries delivered as snapshot then diff, over server-sent events or the socket
  • Retries only for transient failures, and only on idempotent methods

When something goes wrong #

Failures are classified rather than collapsed into a generic 500, because the class is what tells you whether to retry, to fail over or to page someone. Every response carries x-agentycs-request-id, success or failure, and every failure adds x-agentycs-error-code naming the class below.

How the app gateway classifies a failure, and what the client should do about it.
What happenedStatusSafe to retry?
No credential, or one that is invalid, revoked, expired or disabled401 unauthorizedNo. Get a new credential.
A valid identity, but the per-operation permission check denies it403 permission_deniedNo. The answer will not change on its own.
The application did not answer inside the gateway's deadline504Yes, on an idempotent method.
A transport, decode or forwarding failure between gateway and application502Yes, on an idempotent method.
The circuit breaker is open and the gateway is shedding load503Yes, after a backoff.
The authorisation backend itself is momentarily unavailable503Yes. This is explicitly not a deny.
Why a transient auth failure is never a deny

Collapsing an unreachable authorisation service into 403 would teach every client that a temporary outage is a permanent answer, and clients cache that. Deterministic outcomes — invalid, revoked, expired, disabled — deny. A backend that could not be reached returns a temporary status, so a retry is the correct behaviour rather than a bug.

Correlating a failure back to the request

The request id on the response is the same id carried through the gateway, the application runtime and the audit record, so a support conversation starts from one identifier rather than a timestamp and a guess. Quote it and the trace, the logs and the access record all resolve to the same request.

What a denial looks like · Shell
$ curl -i https://agentycs.example.com/api/apps/inspections/close_finding \
    -H "Authorization: Bearer $AGENTYCS_API_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"finding_id": "FAW-014-22"}'

HTTP/2 403
x-agentycs-error-code: permission_denied
x-agentycs-request-id: 01JQ8Z5W3K4M6P8R0T2V4X6Y8A

A valid identity that is not allowed this operation. The class is in the header, not buried in a message, so a client can tell a denial from an outage: 403 will not change on retry, while a 503 from the same endpoint means the authorisation backend was briefly unreachable and a retry is correct. Quote the request id and the trace, the logs and the access record all resolve to this call.

The same rules on every path #

A direct driver connection is not a way around governance. Every path shares one planner and one set of results, so the protocol you pick is a matter of which client you happen to be holding. Row filters, column masks and deny decisions are evaluated during execution, and every read lands in a redacted audit record correlated to the identity behind it.

  • Read-only by default, with mandatory limits and a bounded cost per query
  • Filtering and masking applied identically to SQL, Flight, graph and REST
  • Workspace scope derived from validated session or token state, never a client hint
  • Per-operation permission checks on application queries and mutations
The same query, two identities · SQL
-- Signed in as a regional analyst.
=> SELECT count(*) FROM findings;
 count
-------
  1284

-- Same instance, same table, same SQL, signed in as a site contractor.
=> SELECT count(*) FROM findings;
 count
-------
   112

Nobody wrote a WHERE clause. The filter is part of query execution rather than a layer in front of it, which is why a direct driver connection reaches the same answer as the application does, and no more.