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.
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.
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:
| Field | Required | Meaning |
|---|---|---|
entity | yes | One constructor expression for an entity type, such as Directive("..."). It is the fact's identity and drives search. |
content | yes | The fact body: free text or a JSON string. |
known_from | no | The date the fact became believed, YYYY-MM-DD. Omit it to use today in UTC. |
edges | no | A list of [relation, target] pairs, for example [["Has()","Importance(\"critical\")"]]. |
source_text | no | The 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.
loom --store coding save_many --params-file facts.jsonwhere 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.
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:
| Rule | What it answers | args |
|---|---|---|
facts_by_groups | The general structured read, as above. | the list of groups |
list_types | Which types exist in the store. | [] (or omit) |
text_search | Facts containing some text. | ["Friday",[],"or"]: the text, a list of type filters, and "or"/"and" |
fact_history | The 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_targets | What 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:
| Option | Effect |
|---|---|
when | Read 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_history | Keeps 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. |
full | fact_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.
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).
| Goal | Command | Reversible |
|---|---|---|
| Retract a wrong fact; keep it in history | loom --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 permanently | loom --store coding hard_delete --params '{"fact_id":"<id>"}' | no |
| Add retrieval edges to an existing fact | loom --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.
A quick end-to-end check that a store works:
loom --store coding save ....query and the entity's exact Identity() match.forget, then query again. It should no longer appear in current results.