7.2 The {ai: {…}} directive map — steer and tune a fetch
An LLM-backed edge takes its per-query directives as a nested map under the reserved ai key —
legal Cypher, structurally collision-free with real edge data, and fail-safe (a map is not a storable
property value, so it could never silently filter the read if it leaked):
MATCH (p:Person {name:'…'})-[:RATED]->(seed:Movie)MATCH (seed)-[:SIMILAR_TO {ai: {hint:'obscure, and French', model:'chat_cheap', confidence: 0.8}}]->(rec:Movie)RETURN rec.titleThe flat spellings (ai_hint, ai_model, …) are RETIRED: a query using one is rejected before
execution with the namespace replacement named in the error, so callers self-correct. The ai
namespace is closed: an unknown key inside it is warned about as a probable typo, never silently
ignored. Its keys:
hint— the free-text steer the schema has no property for (a mood, a vibe, a language, an angle), reaching the generator’s prompt as{{ hint }}or folding into an aggregate’s reduce instruction. A soft steer, not a filter: everything the schema can express (genre, year, a rating floor) belongs in an ordinaryWHERE.model— a portable, world-defined role id (e.g.chat_cheap,code_best), resolved through the world’s role map exactly like the realm edge’s ownrole:declaration. A query never pins a concrete model name; an unknown role falls back to the edge’s declared tuning (with a warning), never a failure.- No
temperature. The host sets no sampling temperature; atemperaturekey in the map is stripped with the rest of the steering and warned about as an unknown key, never applied. confidence— raises a generative edge’s confidence floor for this query (“only picks you’re sure of” vs “brainstorm wildly”); below-floor records are dropped before resolution, costing no lookup.fresh: true— bypasses the cross-query TTL cache read (“regenerate my taste summary now” / “re-check today’s availability”); the fresh result still writes through. Applies to ANY TTL-cached producer, LLM-backed or remote.voice— the register/style of a PROSE-producing reduction (“second person, warm”, “a noir narrator”). Aggregate reduce only: names have no voice, and grounded extracts must not be restyled.wordcount— a target length for a prose reduction (clamped to a sane range; a prompt-level target, never a token cap — truncation mid-sentence is worse than a 10% overshoot). On a graph-cached aggregate (§5.5) it is not free text at all: it quantizes to the nearest persisted BAND (gist ~40 / standard ~200 / long ~600), so sized requests stay cacheable — 250 hits the same committed node the directive-free ask created.text: {chars, keep, clean}— how much of each ROW’s text a per-row judgment reads (ai.relevant/ai.score/ai.classify), which part of it, and whether markup is stripped first.keep: 'around'reads the passages nearest the criterion rather than the opening — on a FILTER that is the difference between a degraded answer and a row dropped because its deciding sentence sat too far in. Markup is stripped by default;clean: 'none'keeps it. Same map, same keys, as §7.7.sample: {size, subset}— on a generative edge, how many ANCHORS are rendered into the one prompt that seeds the generation, and which of them. A generative edge is exempt frommaxAnchors(it makes no per-anchor call — every anchor goes into a single prompt), so the prompt is what bounds it: 200 by default, raisable to 1000,subsetone offirst(the default) /last/longest/random. It matters more here than it reads: the generation is seeded by the anchors in the prompt and by no others, so the rest of a larger set is not thinly covered, it is absent — which is why a bounded prompt now returns aPARTIAL_RESULTnote saying how many anchors of how many it was seeded from. The same key, spelled the same way, bounds a holistic aggregation’s evidence (§7.7).
Precedence: query directive → the realm edge’s own declaration (§5.3 generator, aggregate reduce)
→ the deployment default. The directives apply to generative edges (the generator call) and
aggregate edges (the fan-in reduction) alike, for that query only — cached results are keyed by
the full steering, so a chat_cheap, breezy, 40-word run never serves from (or pins) the plain cache
entry. On a graph-cached aggregate (§5.5) the same principle splits three ways: wordcount selects
a persisted band (still cached), fresh regenerates-and-writes-through, and everything semantic
(hint/voice/realm.*/model) makes the result transient — it never touches the
committed canonical node. The whole block is steering, not data: stripped from the executed query, never stamped onto
a materialized edge. And since views are saved queries, a realm can bake any of it into a view
(a CheapRecommendations view with {ai: {model:'chat_cheap'}}) with no extra mechanism.
7.2.2 The {realm: {…}} map — the realm’s own prompt parameters
Section titled “7.2.2 The {realm: {…}} map — the realm’s own prompt parameters”Where ai is the closed embabel-standard namespace, realm is the open one: its keys pass
through verbatim to the producer’s prompt — a generative template variable, or a key: value line
folded into an aggregate’s reduce instruction. The realm defines its own steering vocabulary by simply
referencing the variable in its prompt; an unreferenced key is inert:
-- realm-movie's prompts opt into an `era` parameter ({% if era %} … {{ era }}):MATCH (ts)-[:SUGGESTS {realm: {era: 'the 1970s'}, ai: {model: 'chat_cheap'}}]->(m:Movie)RETURN m.titleRealm parameters are steering like everything above — stripped from the read, never stamped, part of the
cache key — and can never clobber the engine’s reserved template variables (anchors, exclude,
want, hint, …).
For a remote producer that declares scalar queryArgs (string, integer, number,
boolean), keys in the edge’s realm map are request inputs, not prompt parameters or record
identities. Each supplied key must be declared and have a scalar value coercible to its declared
type. It can be a literal, a query parameter, or a property of a node bound in an earlier clause; an
unresolved, missing, non-scalar, or ambiguous value is rejected rather than silently dropped to
make an unfiltered request. A value the query computes (a function call, arithmetic, or a WITH
variable such as a date derived from date()) is not accepted: write it as a literal, for example
{realm: {from: '2026-07-01'}}, and the rejection says so. Omit an unused optional input to retain the source’s default.
An input named in requiredQueryArgs must be supplied: a read without it is refused, naming the
missing {realm: {…}} entry, and never sent bare. For
example, a declared string segment can narrow a contact-list operation:
MATCH (me:AssistantUser)MATCH (me)-[:HAS_MAILBOX_CONTACT {realm:{segment:me.segment}}]->(c:MailboxContact)RETURN c.emailOnly declared realm inputs are forwarded alongside the producer’s own arguments;
distinct input sets stay separate in the cache. A
single query cannot use different input sets for the same producer. A successfully executed
upstream query that binds no input rows makes no downstream request; a virtual upstream
stage that has not executed yet is not evidence of absence and cannot license a broad
request or a clean empty answer. If a graph predicate targets a source field already set by an
explicit request input, that input is not overwritten; a conflicting predicate is still checked
on the fetched graph rows, not claimed as a source-side match. A declared filter is a field named by queryArgs or sourceFilters. When a target-alias
condition on a declared filter was not sent — including a parameter or expression on its right-hand
side — the answer depends on whether the read was complete. After a complete read the graph
applies the filter exactly, so a miss is a real miss and the unsent filter costs only calls and
latency. After a bounded read — a page cap or declared limit reached on a full page, or a
LIMIT-capped fetch that came back full — a read that cannot establish a match carries a
UNPUSHED_FILTER warning, even when the source returned zero records: a bounded page fetched
without that predicate does not prove the requested record is absent. A failed read carries its failure diagnostic; a skipped read does not count as
source evidence. Neither is reported as an unfiltered successful read. A conflicting explicit
realm input preserves its own request value and does not suppress the warning after a
bounded read. Without queryArgs, existing producer-specific realm
steering keeps its ordinary meaning above.
A learned OpenAPI collection exposes scalar query parameters as declared queryArgs
except paging controls and conventional free-text search inputs, which are not treated as
filters; parameters the API marks required are also listed in requiredQueryArgs. A matching equality on a numeric or boolean response field, or an enum-constrained string field,
may be sent as a source filter; an unconstrained text equality is still checked on returned
records. An explicit realm input takes precedence over an inferred filter on the same field:
a conflicting graph predicate remains a graph filter and cannot silently change the request.
A GET of one object beneath a keyed parent (for example, GET /containers/{containerId}/usage)
can expose a navigable singleton edge from that parent even when the returned object has no own
identity key. Its lookup uses exactly one parent key per call, and that key is echoed on each
returned record, so results from different parents cannot join to each other. When the record
already has a property with the key’s name, or the name is reserved, the echo uses a distinct
suffixed property name (for example id_2) and the edge joins on that name.
Learned properties declared as OpenAPI date, date-time, and time remain strings in the graph.
Use ISO string comparisons only where the source guarantees a consistent representation; otherwise
normalize explicitly before comparing or ordering.

