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

Graph tags are visible from the CLI, but not filterable there

inspect prints node and edge tags, and it is the only CLI surface that shows them. Filtering a graph traversal needs the traversal itself, which has no flag form: jnh agents memory query builds only the semantic and log sections, so there is no --filter for a graph element and none is offered. Graph metadata filters are issued through the SDK or API.

inspect takes no filter

inspect enumerates what a scope holds and accepts no predicate, on the API or the CLI. Passing --filter there is refused with a pointer to query, which does narrow.

Multi-scope reads (--scope) reject metadata filters, and the CLI forwards that refusal rather than pre-empting it, so the rule stays the API's. See the note below.

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

Graph start nodes and steps 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).
  • metadata: Matches custom user-defined tags.

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
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.