Selectors and the document graph
Ids and @ref turn a document into a graph. Selectors are a small query language over that graph, short enough for an agent to write without documentation.
Ids
Any block may carry id: "…". Ids match [A-Za-z_][A-Za-z0-9_.-]* and must be unique in a document. Semantic and agent blocks need one. Blocks without an id get a derived id such as doc-2 or slide-1 (the type and its position among blocks of that type). Derived ids are handy in queries but change when you edit the file, so give blocks you refer to a real id.
References and edges
@ref("id") is a value that points at another block. The property that holds it names the edge:
| Edge | Used on | Meaning |
|---|---|---|
evidence | @finding | Blocks that support the claim |
criteria | @decision | Blocks that inform the decision |
supports | @hypothesis | Evidence for |
contradicts | @hypothesis | Evidence against |
depends_on | @requirement, @action | Prerequisites |
produces | @action, @task | Artifacts produced |
source | @sheet, @table, @chart, semantic blocks | Data source or origin |
task | @artifact | The producing task |
A property that takes an array of references also accepts a single @ref. A reference to an id that does not exist is an osf lint error (dangling-ref). Renderers show references as links to the target. In Markdown, link to a block with [text](#id).
$ osf graph report.osf
F-1 --evidence--> budget
F-1 --evidence--> chart-actuals
D-1 --criteria--> F-1
D-1 --criteria--> risksosf graph --dot prints Graphviz, osf graph --json prints { nodes, edges }, and so does graph() in omniscript-core.
Selector syntax
selector = term { "," term }
term = node { ("->" | "<-") edge }
node = "#" id | type { "[" filter "]" } | "*" { "[" filter "]" }
filter = key | key op value
op = "=" | "!=" | "<" | "<=" | ">" | ">=" | "~"typeis a block name:finding,decision,table,doc,code(orosfcode), and so on.*matches every block.keyis a property name or a special key:id,type,tag(an element oftags) andtext(the body text and the block's title-like property).[key]alone means the property is present.~is a case-insensitive "contains". Numbers compare as numbers; everything else, including ISO dates, compares as text. For arrays any element may match; for!=no element may.-> edgefollows references out of the current blocks through that property;<- edgegoes backwards.*as the edge follows every reference property.- Results come back in document order, without duplicates.
Examples
| Selector | Result |
|---|---|
#F-1 | The block with id F-1 |
finding | All findings |
finding[confidence>=0.8] | Findings with confidence of at least 0.8 |
finding[status=confirmed][confidence>0.7] | Filters combine with AND |
finding[tag="nordic"] | Findings tagged nordic |
decision[status=pending] | Open decisions |
decision[deadline<2027-01-01] | Decisions due before 2027 |
#D-1 -> criteria | What decision D-1 is based on |
#D-1 -> criteria -> evidence | The evidence behind those criteria |
sheet <- evidence | Blocks that cite a sheet as evidence |
action[status!=done] | Open actions |
requirement[level=must] | Mandatory requirements |
finding, decision | Union |
*[text~budget] | Any block that mentions “budget” |
Where selectors work
| Surface | How |
|---|---|
| CLI | osf query report.osf 'finding[confidence>=0.8]' [--json] |
| JavaScript | query(text, "finding[confidence>=0.8]") |
| MCP | The osf.query tool |
| A2A | The built-in osf.query skill, and osf.export with a selector |
| Playground | The query box under the editor |
There is no projection or aggregation: you get whole blocks as JSON and do the rest yourself.