FlureeLabs
GuideMarch 30, 2026

Agent JSON Output Format

A query output format for LLM and agent consumption. Declares column types once in a schema header, encodes values as native JSON primitives, and supports byte-budget truncation with resumable pagination.

AI AgentsJSON-LDSpec

Why it exists

Standard SPARQL JSON repeats the datatype on every binding in every row. For a 1000-row result with 5 variables, that's 5000 redundant type strings. In a context window, this wastes tokens.

Fluree's Typed JSON format ({"@value": 30, "@type": "xsd:long"}) solves the ambiguity problem but makes every value an object — also token-expensive for LLMs that mostly just need the value.

Agent JSON takes a different approach: declare types once in a schema header, then use bare JSON primitives in the rows. This gives the LLM type information when it needs it (check the schema) without paying per-row overhead.

It also adds byte-budget truncation. LLMs have finite context windows, and the relevant constraint is bytes (which correlate with tokens), not row count. A query that returns 10 rows of 10KB each is more expensive than 1000 rows of 10 bytes. The byte budget lets the server stop before overflowing the context, and the resume field provides a ready-to-execute SPARQL query to get the next page.


Requesting Agent JSON

Set the Accept header:

Accept: application/vnd.fluree.agent+json

Optionally set a byte budget:

Fluree-Max-Bytes: 32768

Example

curl -X POST http://localhost:8090/v1/fluree/query \
  -H "Content-Type: application/sparql-query" \
  -H "Accept: application/vnd.fluree.agent+json" \
  -H "Fluree-Max-Bytes: 32768" \
  -d '
    PREFIX schema: <http://schema.org/>
    SELECT ?name ?age
    FROM <mydb:main>
    WHERE {
      ?person schema:name ?name .
      ?person schema:age ?age .
    }
  '

Response structure

Complete response (no truncation)

{
  "schema": {
    "?name": "xsd:string",
    "?age": "xsd:integer",
    "?s": "uri"
  },
  "rows": [
    {"?name": "Alice", "?age": 30, "?s": "ex:alice"},
    {"?name": "Bob", "?age": 25, "?s": "ex:bob"}
  ],
  "rowCount": 2,
  "t": 5,
  "iso": "2026-03-26T14:30:00Z",
  "hasMore": false
}

Truncated response (byte budget exceeded)

{
  "schema": {
    "?name": "xsd:string",
    "?age": "xsd:integer"
  },
  "rows": [
    {"?name": "Alice", "?age": 30},
    {"?name": "Bob", "?age": 25}
  ],
  "rowCount": 2,
  "t": 5,
  "iso": "2026-03-26T14:30:00Z",
  "hasMore": true,
  "message": "Response truncated due to size limit of 32768 bytes. Use the query below to retrieve the next batch.",
  "resume": "SELECT ?name ?age FROM <mydb:main@t:5> WHERE { ?s schema:name ?name ; schema:age ?age } OFFSET 2 LIMIT 100"
}

Envelope fields

FieldTypeWhen presentDescription
schemaobjectAlwaysMaps each variable to its XSD datatype. Declared once for all rows.
rowsarrayAlwaysArray of {variable: value} objects using native JSON types.
rowCountintegerAlwaysNumber of rows in this response.
tintegerSingle-ledger queriesTransaction number the query ran against. Omitted for multi-ledger queries (each ledger has its own timeline).
isostringAlwaysISO 8601 wallclock timestamp at query time.
hasMorebooleanAlwaysWhether additional rows exist beyond what was returned.
messagestringWhen truncatedHuman-readable explanation.
resumestringWhen truncated, single-FROM onlySPARQL query with @t: time-pinning and OFFSET, ready to execute for the next page.

Schema types

The schema object maps each variable to its datatype:

Schema valueMeaning
"xsd:string"String literal
"xsd:integer"Integer
"xsd:long"Long integer
"xsd:double"Double-precision float
"xsd:decimal"Decimal
"xsd:boolean"Boolean
"xsd:date"Date
"xsd:dateTime"DateTime
"uri"IRI reference

When a variable has mixed types across rows, the value is an array:

"?value": ["xsd:string", "xsd:integer"]

Value encoding

Values use native JSON types for anything the JSON type system can represent unambiguously:

RDF typeJSON encodingExample
xsd:stringJSON string"Alice"
xsd:integer, xsd:longJSON number30
xsd:double, xsd:decimalJSON number1.68
xsd:booleanJSON booleantrue
IRI (uri)JSON string (compacted via @context)"ex:alice"

Types that JSON can't represent natively get an inline annotation:

{"@value": "2024-05-15", "@type": "xsd:date"}
{"@value": "Alicia", "@language": "es"}

The distinction: the schema header tells you the type of a variable across all rows. Inline annotations appear only on individual values when the type would otherwise be ambiguous.


Byte budget

When Fluree-Max-Bytes is set, the formatter serializes rows one at a time. When cumulative serialized row data exceeds the budget, it stops and sets hasMore: true.

The budget covers row data only — schema, envelope fields, and metadata are excluded.

When truncation occurs:

  • message explains what happened.
  • resume (for single-FROM queries) contains a SPARQL query with:
    • @t: pinning to the same transaction number, so the next page sees identical data even if new transactions have committed.
    • OFFSET equal to this response's rowCount.
    • LIMIT set to 100 (default batch size).

The caller executes the resume query to get the next page. Repeat until hasMore is false.

Why @t: pinning matters

Without time-pinning, new transactions between page requests could shift rows — causing duplicates or gaps. The resume query pins to the original transaction number so pagination is consistent.


Multi-ledger behavior

When a query has multiple FROM clauses:

  • t is omitted — there's no single transaction number that covers all ledgers.
  • resume is omitted — time-pinning would require @t: on each FROM clause individually, and the server doesn't know the original query's intent well enough to generate this.
  • message (when truncated) suggests using @iso: on each FROM clause for time-consistent pagination.

The iso field is always present regardless of how many ledgers are involved.


Rust API

use fluree_db_api::{FormatterConfig, AgentJsonContext};

let config = FormatterConfig::agent_json()
    .with_max_bytes(32768)
    .with_agent_json_context(AgentJsonContext {
        sparql_text: Some(sparql.to_string()),
        from_count: 1,
        iso_timestamp: Some(chrono::Utc::now().to_rfc3339()),
    });

let result = db.query(&fluree)
    .sparql("SELECT ?name ?age WHERE { ?s ex:name ?name ; ex:age ?age }")
    .format(config)
    .execute_formatted()
    .await?;

Or on a QueryResult directly:

let json = result.to_agent_json(&snapshot)?;                       // no budget
let json = result.to_agent_json_with_config(&snapshot, &config)?;  // with budget

FormatterConfig::agent_json() is one of four format constructors — the others are jsonld(), sparql_json(), and typed_json(). All work with .format() on db.query(), dataset.query(), and fluree.query_from().


Comparison with other output formats

JSON-LDSPARQL JSONTyped JSONAgent JSON
Type infoInferable types bare; non-inferable annotatedEvery value wrapped with type/datatypeEvery value wrapped with @value/@typeSchema header once; bare values in rows
Token costLowHighHighLow
Type precisionPartial (strings and numbers implicit)FullFullFull (via schema)
Byte budgetNoNoNoYes
Resume paginationNoNoNoYes
Time-pinningNoNoNoYes (@t: in resume)
Intended consumerApplicationsSPARQL toolingTyped struct deserializationLLMs / agents