Skip to content

Metadata

Metadata allows tagging memory items along custom dimensions such as session IDs, source documents, tenant IDs, dates, or run IDs. You attach string key-value pairs to items when writing, and filter read queries using those tags.

Metadata is supported uniformly across all memory types:

Item kind Written in Filtered by
Vector chunk vectors[].metadata semantic.filters
Graph node graph.nodes[].metadata graph.start.metadata, graph.steps[].node.metadata
Graph edge graph.edges[].metadata graph.steps[].metadata
Execution-log step log.metadata log.metadata

The metadata schema, filter grammar, and query semantics are identical across all four memory types.

Cross-section queries with uniform tags

Tags applied consistently across memory types allow single queries to retrieve related vector chunks, graph facts, and execution log steps for a given session or context.

Tagging

Provide a metadata map of key-value string pairs alongside the item payload:

{
  "log": {
    "stepId": "step-104",
    "thoughtProcess": "checked the invoice totals",
    "metadata": { "session": "s-991", "source": "slack" }
  },
  "vectors": [{
    "chunkId": "daily-2026-07-30",
    "rawContent": "Alice fixed duplicate invoice lines in billing.",
    "metadata": { "session": "s-991", "employee": "alice", "day": "2026-07-30" }
  }],
  "graph": {
    "nodes": [{
      "nodeId": "alice",
      "label": "Person",
      "metadata": { "session": "s-991" }
    }],
    "edges": [{
      "edgeId": "alice-fixed-billing",
      "sourceNodeId": "alice",
      "targetNodeId": "billing",
      "relationshipType": "FIXED",
      "metadata": { "session": "s-991" }
    }]
  }
}

Full-set replacement on write

Writing an item sets its metadata map to the provided payload. Any key omitted in subsequent writes is removed. To preserve existing tags when updating content, provide the complete metadata set. To clear metadata deliberately, send an empty map.

Execution log steps

Because execution log steps are append-only, log step metadata is written once and cannot be modified.

Filtering

Filters specify a key, value, and operator:

{
  "semantic": {
    "queryText": "what did alice ship?",
    "limit": 8,
    "filters": [{ "key": "session", "value": "s-991" }]
  },
  "log": {
    "limit": 20,
    "metadata": [{ "key": "session", "value": "s-991" }]
  },
  "graph": {
    "start": {
      "label": "Person",
      "metadata": [{ "key": "session", "value": "s-991" }]
    },
    "steps": [{
      "relationshipType": "FIXED",
      "metadata": [{ "key": "session", "value": "s-991" }],
      "node": { "metadata": [{ "key": "session", "value": "s-991" }] }
    }]
  }
}

The operator field defaults to equality (OPERATOR_EQUALS). Multiple filters combine with AND logic; OR conditions and negation (!=) are not supported. For disjunctive queries, issue separate requests or tag records with shared values.

Ranges

Set operator to OPERATOR_LESS_THAN, OPERATOR_LESS_THAN_OR_EQUAL, OPERATOR_GREATER_THAN, or OPERATOR_GREATER_THAN_OR_EQUAL. A bounded range uses two filters on the same key:

{
  "filters": [
    {
      "key": "day",
      "value": "2026-07-01",
      "operator": "OPERATOR_GREATER_THAN_OR_EQUAL"
    },
    {
      "key": "day",
      "value": "2026-07-31",
      "operator": "OPERATOR_LESS_THAN_OR_EQUAL"
    }
  ]
}

Lexicographic comparison

Metadata values are stored and evaluated as strings. Ordered comparisons evaluate byte-order sorting:

  • ISO-8601 dates and timestamps: "2026-07-01" < "2026-07-15" (Valid)
  • Zero-padded numbers: "007" < "042" (Valid)
  • Unpadded numbers: Evaluated lexicographically ("10" < "9")
  • Ordinal words: Evaluated alphabetically ("High" < "Low" < "Medium")

Encode values according to the intended ordering (such as ISO-8601 timestamps, fixed-width zero-padded numbers, or sortable prefixes like "1-high").

Missing keys

Items without the specified key do not match and are excluded from results across all operators.

Pre-filter execution

Filters reduce candidate records before ranking or pagination limits apply:

  • Semantic: Evaluated before vector distance ranking. The nearest limit chunks among matching records are returned.
  • Graph: Evaluated on candidate nodes or edges before traversal expands. Non-matching elements are pruned prior to expansion.
  • Log: Evaluated before limit selection. Requesting 20 steps with session=s-991 returns the 20 most recent steps matching that session.

Returning fewer items than limit reflects the matching record count and does not indicate truncation.

Returned metadata

Queries and inspection listings return metadata tags alongside each item:

{
  "rows": [{
    "n0_id": "alice",
    "n0_label": "Person",
    "n0_metadata": { "session": "s-991" },
    "e0_id": "alice-fixed-billing",
    "e0_type": "FIXED",
    "e0_metadata": { "session": "s-991" }
  }]
}

In the console

The console Memory Inspector displays metadata tags for all item kinds and supports tag-based filtered reads. Graph queries with metadata filters are executed via the SDK or API.

From the CLI

Tag a single execution-log step as it is written with --metadata key=value, repeated per pair. Both supersessions, jnh agents memory supersede-chunk and jnh agents memory supersede-edge, take the same flag, carrying the replacement's whole tag set. Anything larger, including tagged vector chunks and graph elements, is written from a file with jnh agents memory commit --from-file, where the metadata maps are the ones shown above.

$ jnh agents memory add-step my.agent --step-id s-104 \
    --tool roster.read --thought "checked the roster" \
    --metadata session=s-991 --metadata day=2026-07-15

On a supersession the set is the replacement's own: it is written whole rather than merged with the superseded item's tags, and the superseded item keeps the tags it had.

$ jnh agents memory supersede-edge my.agent \
    --prior e-1 --new-id e-2 --valid-at 2026-08-01 \
    --source alice --target acme --type WORKS_AT \
    --metadata source=hr-announcement --metadata confidence=high

Read side, --filter key<op>value narrows a query, repeated per predicate:

$ jnh agents memory query my.agent --log-limit 10 \
    --filter session=s-991 --fields stepId,toolUsed,metadata

STEP   TOOL         TAGS
s-104  roster.read  day=2026-07-15, session=s-991

The operators are =, <, <=, >, and >=, spelled inside the flag value rather than in a second flag. Repeated --filter flags combine with AND, matching the API. A bounded range is two of them on one key:

$ jnh agents memory query my.agent --text "what did alice ship?" \
    --filter 'day>=2026-07-01' --filter 'day<=2026-07-31'

The split falls on the key side of the first operator character. A metadata key cannot contain one (the key format is [A-Za-z0-9_.-]), so a value holding = or : is data and needs no escaping beyond your shell's quoting: --filter 'token=YWJjZA==' and --filter 'asserted>=2026-07-01T09:30:00Z' both mean what they look like. There is no inequality operator, and key!=value is refused locally rather than sent, because the API's grammar has no negation.

A filter applies to every section the command requested. One --filter narrows both the semantic and log sections of a single query, which is the point of one tag mechanism across memory types:

$ jnh agents memory query my.agent --text "roster team" --log-limit 5 \
    --filter session=s-991

Narrowing one section and not the other is not expressible in one invocation; run two commands.

An item's tags are printed wherever the item is, by both query and inspect, in a TAGS column that appears only when something in that listing carries a tag. A workspace that tags nothing sees the output it always did. The column is a rendering and is truncated like any other free-text cell; -o json and -o ndjson carry the tag map whole.

$ jnh agents memory inspect my.agent --graph

NODE   LABEL   UPDATED                      TAGS
alice  Person  2026-08-24T06:17:48.697257Z  session=s-991

jnh agents memory query accepts graph traversals as JSON documents via --graph-json, using a file path or standard input (-). Tag filters are specified within the query document on target elements (start.metadata and steps[].node.metadata for nodes, or steps[].metadata for edges). Standalone traversal queries do not require --text:

$ jnh agents memory query my.agent --graph-json - <<'EOF'
{
  "start": {
    "label": "Person",
    "metadata": [{"key": "session", "value": "s-991"}]
  },
  "steps": [{
    "relationship_type": "WORKS_AT",
    "node": {"label": "Company"}
  }]
}
EOF

N0_ID  N0_LABEL  E0_ID  E0_TYPE   E0_VALID_AT           N1_ID  N1_LABEL
alice  Person    e-1    WORKS_AT  2026-08-01T00:00:00Z  acme   Company

Each row represents a matched path. Columns display traversal bindings in path order: anchor node N0, followed by edge E0 and reached node N1 for each step. Because output columns are determined by the query document, --fields and -o name are not supported on graph results; use -o json or -o ndjson (where records include "section":"graph").

--filter compatibility with --graph-json

--filter applies across all requested query sections and cannot attach to individual graph traversal steps; combining --filter with --graph-json returns an error. The JSON document is validated strictly: unrecognized fields in GraphQuery (such as "lable") return an error rather than being ignored.

inspect takes no filter

inspect enumerates all stored records in a scope and does not accept filter predicates on the API or CLI. Use query to filter records.

Multi-scope reads (--scope) do not support metadata filters. See Limits and rules.

Metadata vs. payload fields

Field Purpose Filterable
rawContent (chunk) Text passage embedded for vector search No (vector search target)
properties (node, edge) Structured JSON data payload No
metadata (all kinds) Flat key-value string tags Yes

Use metadata for flat key-value pairs intended for search filtering. Use properties for structured or nested JSON payload data that does not require platform filtering.

Graph query filter lists

Node matches (start and each step's node) accept two separate filter lists:

{
  "start": {
    "filters":  [{ "key": "Label", "value": "Person" }],
    "metadata": [{ "key": "session", "value": "s-991" }]
  }
}
  • filters: Matches first-class schema columns (NodeId, Label, NodeType). NodeType is an entity's class, set by a memory vocabulary; it is a column rather than a tag, so it is filtered here and not through metadata.
  • metadata: Matches custom user-defined tags.

A step itself carries only metadata, which filters the edge the hop traverses. To filter the node a step reaches, use that step's node.filters and node.metadata.

Naming conventions

Common tagging conventions include using keys such as session or run. Jennah does not reserve specific key names; all metadata keys follow standard indexing and filter rules.

Limits and rules

Constraint Detail
Keys per item 32
Filters per section 8
Key format [A-Za-z0-9_.-], 1-128 characters
Value format Any string, up to 2,621,440 characters (longer values are rejected with INVALID_ARGUMENT)
Operators OPERATOR_EQUALS
OPERATOR_LESS_THAN
OPERATOR_LESS_THAN_OR_EQUAL
OPERATOR_GREATER_THAN
OPERATOR_GREATER_THAN_OR_EQUAL
Combining AND logic only
Missing key Excluded from results
Write semantics Full-set replacement (append-only items are written once)
Erasure Deleted with the item or scope

Unsupported operators return UNIMPLEMENTED errors.

Metadata filters are single-scope

Metadata filters are supported for single-scope queries. Multi-scope queries do not support metadata filters.

Erasure

Metadata is stored with its associated item and is automatically deleted when the item or scope is deleted.