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
limitchunks 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
limitselection. Requesting 20 steps withsession=s-991returns 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:
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).NodeTypeis an entity's class, set by a memory vocabulary; it is a column rather than a tag, so it is filtered here and not throughmetadata.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_EQUALSOPERATOR_LESS_THANOPERATOR_LESS_THAN_OR_EQUALOPERATOR_GREATER_THANOPERATOR_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.