Skip to main content
Every write normally names its space. When the caller does not know which space a memory belongs in, send 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.
Routing is opt in, per request. A write that names a 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 of space_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.
POST /v1/record/batch routes the whole batch as one unit and files every item together, but its response carries job_id, job_ids, status and accepted only. There is no routed_to on a batch response. Items cannot be routed individually.

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.
A memory that lands on the fallback stage is stored with "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_enabled set (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.
Spaces shared with you by another organization are candidates only when the owner set 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_id or a tag is better as a rule: it is free, instant and exact.
Fan-out to several spaces, an inverted index over your memories, and a tool for calibrating the confidence threshold do not exist. 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.
Exactly one of 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.
max_targets is a ceiling, not a count, and each space searched is charged. Raising it does not widen every search: a question that plainly belongs to one space still searches one. What it bounds is the worst case. Set it to 1 to keep a routed read to a single space, or set your organization’s default in the dashboard under Profile → Automatic search.
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 absent on a retrieve that named a space_id. It is not null. The key does not appear in the response body at all, so a default retrieve is byte-for-byte what it always was. Test for the key’s presence rather than for a null value.
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 a 200 with an empty results array and exactly one searched entry whose space_id is null:
The entry is still there, rather than searched being an empty array, because the reason has to be carried somewhere.
An abstaining read deliberately does not search your default space. This is the one place read routing differs from write routing, and it is easy to mistake for the feature being broken.On a write, your default space is the right answer when nothing fits: it is the sink, so filing there is correct by definition. On a read it is the opposite. It is the bag of everything that fitted nowhere, which makes it the least topically coherent space you have and therefore the worst place to look for an answer to a specific question. Irrelevant memories are worse than none, so Anona returns none and tells you why.Widen deliberately when you want that behaviour: name a space with space_id, or pass fallback_space_id.

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 on POST /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.
In TypeScript, { 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

The retrieve 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:
An addressed call carries no such line, so an agent parsing today’s replies sees nothing new. When nothing was chosen, the tool answers in prose instead of returning memories: it says that no space was searched, why, and that naming a space with space_id or passing fallback_space_id will search one anyway.

Routing settings

Each space has an optional routing profile.
Authenticate with an API key or a dashboard session. You must be the owner of the space: a shared space’s visitor cannot change how it is routed into. These endpoints are configuration, not metered work, so they cost no credits and keep working when an organization is out of credits, which is what lets you turn routing off at any time. 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

PUT is a full replace, not a patch. A field you omit is cleared, and an omitted flag returns to its default. Send the complete state you want.

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.
The id is checked against your own spaces, so a typo is a 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:
Branch on 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.