route: "auto" instead of space_id and Anona chooses
one for you. A search can be routed the same way: see
Searching without naming a space, which is
the same machinery with one deliberate difference in what happens when nothing
fits.
space_id behaves exactly
as it always has: no routing runs, nothing extra is called, and routed_to is
null.
Automatic routing is available on
POST /v1/record, POST /v1/record/batch,
POST /v1/retrieve, and the MCP record and retrieve tools. The Python and
JavaScript SDKs expose it on all three calls: route="auto" with
fallback_space_id in Python, route: "auto" with fallbackSpaceId in
JavaScript.If every routed write comes back with
reason: "router_disabled", automatic
routing is not enabled on the deployment you are calling. route: "auto" is
still accepted and the write still succeeds, but the memory goes to your fallback
space and nothing is sent to the decision provider. That reason means off, not
broken, and needs no change on your side. A routed read on such a deployment is
not so forgiving: with no fallback_space_id it is 422 no_route_target, because
a read has no default rung to fall to.Naming a target
Exactly one ofspace_id or route must be present.
Sending both, or neither, is
422 route_conflict. Sending max_targets: 2 or
3 is 422 validation_error: the field is capped at 1, and a value the API
would have to ignore is refused rather than accepted quietly.
Reading routed_to
POST /v1/record returns a routed_to array whenever route was used. It is
null on a write that named its space.
There are three
stage values and no others:
reason says why that stage answered. A rule match names the rule that
matched, a confident model pick carries null, and a fallback names what stopped
anything else from claiming the memory:
Every one of these still stores the memory. A fallback changes where it lands,
never whether it lands.
routed_to is the only way to notice a misroute. A memory filed into the wrong
space looks exactly like a correct write from every other angle, so log it, or
surface it, on any path where routing matters.
The cascade
Routing runs three stages in order, and stops at the first that answers.1
Declared rules
Your own rules are evaluated first, over your candidate spaces in
deterministic order by space id. The first match wins. This costs nothing
and calls no model.
2
The decision model
The memory text and each candidate space’s description are sent to a
decision model, which picks one space or abstains. An abstention, or an
answer below the confidence threshold, falls through. See
Subprocessors for what leaves Anona on this step.
3
Fallback
fallback_space_id if you sent one, then your organization’s
default_space_id, then a space literally named default if you have one.
If none of those exist, the write is refused with 422 no_route_target."anona_routed": "fallback" in its metadata, so you can list what needs
filing by hand later. That mark is only trustworthy because you cannot write it
yourself: metadata keys beginning with anona_ are reserved and rejected with
422 reserved_metadata_key.
If you have no candidate spaces at all, no model is called and the memory goes
straight to the fallback. An organization whose only space is its default is in
exactly that position, and routing costs it nothing.
Which spaces can be chosen
A space is a candidate when all of the following hold:- It has a routing profile with
auto_route_enabledset (the default when a profile exists). - Its rendered description is not empty. A space with nothing to say about itself does not compete.
- It is not your organization’s default space. The default is the sink that receives everything unclaimed, so it is never also a target.
shared_auto_route on them, and your membership role has to allow
what is being routed:
That is the only difference in candidacy between the two directions. Everything
else on this list, including the exclusion of your default space, applies to
both.
The router never creates a space. Every answer, whether from a rule or from the
model, is checked against the candidate list that was sent before anything is
written, so a name nobody offered is discarded rather than acted on.
What makes routing work well
The description a space is judged on is built primarily from terms derived from the space’s own contents. Anona reads the entity graph it already builds on every write, refreshes each space’s terms in the background, and adds the terms it sees on writes (tag names,agent_id values and the top level keys of
metadata, never memory content and never user_id or session_id values).
A charter you write adds to those terms, it does not replace them. The
rendered description is your charter, then the terms. This is deliberate: a
short charter on its own (“eng”, “ops”, “money”) describes a space far worse
than its own contents do, so nothing you write can displace what was derived.
Practical consequences:
- A new, empty space is not routable yet. It has no contents to derive terms from. Write to it by name a few times, or give it a charter and topics, and it joins the candidate set.
- Write a charter in sentences. It prefixes the terms, so a sentence about what the space is for adds information the terms cannot carry.
- Use rules for the decisions that are not judgement calls. Anything keyed
on
agent_id,user_id,session_idor a tag is better as a rule: it is free, instant and exact.
max_targets is the field
where fan-out would live, which is why it is capped at 1 today rather than
absent.
Searching without naming a space
POST /v1/retrieve takes the same pair of fields. Send route: "auto" in place
of space_id and Anona picks the space the question is most likely answered
from, searches that one space, and reports which one it was.
space_id or route must be present, as on a write. Both, or
neither, is 422 route_conflict.
A routed read searches the spaces that look likely to hold the answer — usually
one, sometimes two. searched carries an entry per space, best first, and
max_targets caps how many there may be.
Results are ranked across the spaces rather than concatenated, so the leading
memory need not come from the leading space — a space that scored higher as a
space can hold a less relevant memory than one that scored lower.
Reading searched
searched is the only way to notice that a query was answered from the wrong
space. A wrong-space answer is indistinguishable from a space that simply holds
nothing, so read it, or log it, wherever routed reads matter.
The three stages mean the same thing here as on a write:
Not every rule can match a query. A rule’s
field is matched against the
request, and a retrieve request has tags, user_id, agent_id and
session_id but no content, so a rule keyed on content never fires on a
read. The query text is not a rule field; it goes to the model.
Every reason a routed read can report
When a space was searched:
When no space was chosen,
reason carries one of six values. What happens next
depends on whether you sent a fallback_space_id:
The split is deliberate.
no_space_fits and low_confidence are answers: the
model looked at your spaces and reported that none of them holds this. The other
four are not answers about your memories at all. no_candidates means no space
of yours is eligible to be routed to, router_disabled means routing is not
switched on for the deployment you are calling, answer_outside_candidates means
the model named a space that was never offered to it, and model_unavailable
means the decision provider could not be reached. Reporting any of those as “no
memories match” would describe a configuration or availability problem as your
data, so they are refused instead.
When no space fits
An abstention comes back as a200 with an empty results array and exactly one
searched entry whose space_id is null:
searched being an empty array, because
the reason has to be carried somewhere.
fallback_space_id on a read
fallback_space_id is where a routed read looks when nothing was chosen. On a
read it has no default at all, for the reason in the warning above: on a
write the ladder continues to your organization’s default space and then to a
space named default, and on a read neither of those rungs exists. Leave it
unset and an abstention honestly returns nothing.
Sending one turns every row of the six-reason table into a search of that space,
including the four that would otherwise be 422 no_route_target.
An abstained query is still charged. A retrieve that names a space and
matches nothing is charged today, and a routed retrieve that matches nothing is
charged the same way, for consistency rather than for revenue. The usage row is
grouped under your organization’s default space so it stays attributable, or
under
unrouted for an organization that has no default space set.Only retrieve is routable
Read routing exists onPOST /v1/retrieve and the MCP retrieve tool, and
nowhere else. POST /v1/reason requires a space_id, as do the SDKs’
get_context and retrieve_receipt, which are renderings of retrieve that do
not take route. Those calls are addressed only: there is no route field to
send, and sending one is a validation error.
From the SDKs
retrieve still returns a list, so existing code is unaffected. searched rides
on that list as an attribute, and is None on an addressed read.
{ spaceId } and { route: "auto", fallbackSpaceId } are two
arms of a union, so sending both or neither is a compile error as well as a
runtime one.
From MCP
Theretrieve tool takes route and fallback_space_id too, and this is where
they matter most: a model holding the tool sees a list of space ids and none of
the reasoning behind them, so “I do not know which space holds this” is its
normal state.
A routed call prefixes its reply with the space that was chosen and the stage
that chose it, so the model can tell a wrong-space answer from a right one:
space_id or passing fallback_space_id will search one anyway.
Routing settings
Each space has an optional routing profile.GET on a space that has never been configured returns the defaults rather than
a 404. PUT additionally checks that the space really exists, so it needs an
active API key on the organization and answers 404 space_not_found for a
misspelled id instead of storing a profile nothing would ever read.
The settable fields
Rules
Matching is case insensitive.
in tests membership of the array in value; the
other three operators given an array match if any element does. Regular
expressions are not an available operator.
A rule whose space_id names a space that is not a candidate is skipped, not
obeyed. That includes your default space, which is never a routing target.
The derived fields
GET and PUT also return the server-owned half of the profile. These are read
only: they are absent from the request body, and sending one is rejected.
rendered_criteria is worth reading. It is what the decision model actually
sees, so it is the thing to look at when a space is being chosen too often or
never at all.
DELETE removes the profile entirely, which takes the space out of routing
until it is configured again. Deleting a profile that does not exist is still a
204.
Setting a default space
The fallback ladder’s second rung is your organization’s default space.404 space_not_found
rather than a stored id that nothing can serve. Owners and admins only.
Errors
Errors use the standard envelope, with lowercase snake_case codes:
code. message is written for a human and its wording may change.
Routing itself never breaks a write. A dead rule, an unreachable decision model
or an unreadable answer all fall through to the fallback. no_route_target is
the one exception, and it means there is genuinely nowhere to put the memory.
The same holds for a read: a failure inside routing cannot break a retrieve that
would otherwise have worked. It falls through the same ladder, which ends in
fallback_space_id if you sent one, and otherwise in either empty results with a
reason or 422 no_route_target, depending on which reason it was.