Loom keeps your agents on track.

Save and load facts

This page shows how to put a fact into a Loom store and get it back, from the command line and through your agent.

Most of the time your agent saves and reads for you. The commands below are what it runs underneath, and they are useful for checking that a store works.

Choose the store#

On the command line, you name the store yourself: pass --store NAME, or a store field inside --params. With neither, a command that writes or reads a store's facts stops with loom_save requires --store or a store field in --params (exit status 2). The command line does not consult the default key or a [workspace] mapping. query needs no --store, because its namespace already names the store (for example coding_root).

Your coding agent and its hooks choose differently, in this order: a workspace mapping in stores.toml (details), then the top-level default key.

In the examples, coding is the default store of a fresh account. Substitute your own store name.

Save one fact#

loom --store coding save --params '{"entity":"Directive(\"Never deploy on a Friday\")","content":"Agreed in the release retro; Friday deploys caused two outages."}'

A successful save returns the new fact's id:

{
  "fact_id": "<id>"
}

The fields:

FieldRequiredMeaning
entityyesOne constructor expression for an entity type, such as Directive("..."). It is the fact's identity and drives search.
contentyesThe fact body: free text or a JSON string.
known_fromnoThe date the fact became believed, YYYY-MM-DD. Omit it to use today in UTC.
edgesnoA list of [relation, target] pairs, for example [["Has()","Importance(\"critical\")"]].
source_textnoThe verbatim words the fact came from.

If your store is more than 90% full, the result also carries a capacity_warning.

The first save on a computer downloads the embedding model; standard error shows the download's log: HTTP Request: GET/HEAD https://huggingface.co/... lines (some answered 307), a Hugging Face Hub unauthenticated-requests warning and a Fetching 5 files: ... progress bar. Later saves print nothing on standard error. Standard output is only the JSON result. Errors are plain messages with exit status 2: a missing content gives missing a required argument: 'content', an unknown entity type gives an unknown type message, and a bad known_from is rejected with an explanation.

Save many facts at once#

loom --store coding save_many --params-file facts.json

where facts.json contains:

{
  "facts": [
    {"entity": "Decision(\"Use SQLite for the cache\")", "content": "Simple, no server to run."},
    {"entity": "Decision(\"Skip the ORM\")", "content": "Raw queries are enough here."}
  ]
}

Each item takes the same fields as save. Use --params-file for anything longer than a line; it avoids shell quoting problems.

The result is a list aligned to your input. Each element is {"index": 0, "fact_id": "<id>"} on success or {"index": 0, "error": "<reason>"} on failure. Check every element for error.

Load facts back#

Reading goes through query, which runs a named rule against a store. The namespace is the store name plus _root (built-in rules) or _user (rules you wrote):

loom query --params '{"namespace":"coding_root","rule_name":"facts_by_groups","args":[[[["SubTypeOf()","Directive"]]]]}'

That returns the current facts whose entity is a Directive. The args value is a list of groups, each group a list of [relation, target] pairs: pairs inside a group are alternatives, and groups are all required.

Useful built-in rules:

RuleWhat it answersargs
facts_by_groupsThe general structured read, as above.the list of groups
list_typesWhich types exist in the store.[] (or omit)
text_searchFacts containing some text.["Friday",[],"or"]: the text, a list of type filters, and "or"/"and"
fact_historyThe audit trail for matching facts. Add "include_history":true to keep superseded, disputed and retracted rows too (retracted rows show current_status: "retracted" and retracted_at); without it only current rows are returned.[["Directive"],"or"]
edge_targetsWhat a fact points at.["<fact_id>","Has"]: the relation name without parentheses ("Has()" returns nothing)

Calling a rule with the wrong args shape fails. From v0.674 text_search, fact_history and edge_targets give a usage error such as rule 'text_search' was called with the wrong number or shape of args (expected text_search(needle, label_exprs, mode)); pass only the middle arguments, never dbpath or when (earlier releases gave a raw MethodError message); facts_by_groups gives loom julia_exception: query: each leaf must be [relation, target], got "x" for a malformed leaf, and the same wrong-number-or-shape usage error as above (... (see this tool's description for the rule's arguments); pass only the middle arguments, never dbpath or when) when args is empty. Check the shapes above.

query and --store#

query takes no --store. Passing one is an error (loom_query has no store parameter; ...). For the other verbs, a missing store prints an argparse usage dump first, then a message with the prefix loom: error:.

Query options inside --params:

OptionEffect
whenRead as of a date, YYYY-MM-DD. Default is today. facts_by_groups and, from v0.674, text_search honour it: only facts known by that date match. Earlier releases ignored it in text_search.
include_historyKeeps superseded and disputed rows that multi-row results otherwise drop. On facts_by_groups it does not bring back retracted facts; read those with the fact_history rule and "include_history":true.
fullfact_history and facts_by_groups shorten each fact's content to 240 characters (content_truncated: true, content_len) when the whole result is large (measured: nothing is shortened up to about 16 KB of results; at about 380 KB both shorten). Pass "full":true to get the content whole.

Small results come back whole. If a large result shows content_truncated: true, add "full":true to the fact_history or facts_by_groups query to read the content in full.

Search by meaning#

When you know what you want but not its type, use the semantic read:

loom --store coding read_probe_bundle --params '{"query_text":"deployment rules","max_facts":8}'

The result separates current_facts from stale_context. Use current_facts for current-state answers. Provide at least one of query_text, label_queries or label_exprs; a wrong shape for the last two is rejected with a message that shows the expected shape (v0.674 or later).

Correct and remove#

GoalCommandReversible
Retract a wrong fact; keep it in historyloom --store coding forget --params '{"fact_id":"<id>"}'not directly: there is no un-forget. Saving the same entity again makes a new fact with a new id and leaves the old one retracted
Erase a fact and its edges permanentlyloom --store coding hard_delete --params '{"fact_id":"<id>"}'no
Add retrieval edges to an existing factloom --store coding add_edges --params '{"fact_id":"<id>","edges":[["Has()","Importance(\"critical\")"]]}'yes (remove_edges)

forget returns a JSON string starting ok: (followed by the entity it retracted, in canonical form), or noop_already_retracted: <entity>, or noop_no_such_fact. The last means the id matched nothing, so nothing was retracted. The ok: text names the entity that was retracted, so check it to confirm you hit the right fact. To see retracted facts, use the fact_history rule with "include_history":true.

add_edges and remove_edges return ["<fact_id>"]; since v0.674 loom add_edges exits 3 when any edge in the result is an {"error": ...} entry; remove_edges on a fact that is not live returns [-1] with exit status 0. add_edges on a fact id that is not live returns [{"error":"source_fact_not_live"}] (exit status 3 from v0.674, 0 before, so on older releases check the result rather than the exit status). hard_delete returns {"fact_id": ..., "existed": ..., "status": "ok"}, or "status": "noop_no_such_fact" when the id matched nothing.

To roll back an edit to the store's own rules and types (not facts), see revert and schema_log in the CLI reference.

Check the round trip#

A quick end-to-end check that a store works:

  1. Save a fact with loom --store coding save ....
  2. Read it back with query and the entity's exact Identity() match.
  3. Retract it with forget, then query again. It should no longer appear in current results.