Introduction to the Roam Alpha API
RoamAlphaAPI is a javascript API available within Roam that allows querying of a Roam database using Datomic/Datalog.
This page is a summary of what I learned writing RoamPageSearch. I am not a clojure/datalog expert, and my javascript is pretty iffy, but all the examples work so hopefully this might be of use to some people.
Roam Alpha API exposes two datomic functions: q - allows querying of the Roam graph. pull- allows pulling of details of one block
Getting started with Roam Alpha API
The easiest way to get started is to try some examples in your browser developer console, after bringing up the Roam page in your browser. In Chrome, hit ⌘⌥+ I to bring up the developer tools , choose the console tab, and paste some of the examples below at the prompt “>”.
You can navigate the results of the queries in the console by clicking on them to expand, or you can write javascript code to do interesting stuff with the results.
Each of the examples below can be copied and pasted into your Roam console. Note that the examples must be on one line.
A word of warning: if setting variables in the console, avoid short variable names as they may collide with variables Roam uses. If this does happen during your travels, just refresh the page.
You can paste the examples below into the console, however note that you need to use the version without line breaks.
Finding all pages
Find all blocks with a :node/title element (ie all pages), and return their database ids. window.roamAlphaAPI.q('[:find ?e :where [?e :node/title] ]')
In this example,
[?e :node/title] matches “any block with a field called :node/title. ?e is a variable referring to the block that matches
The example returns an array of arrays- the outer array has an entry for each page, and the inner array has one element which is the database id of the page.
Getting Details of One Block
If you know the database id of one block, you can query all details of that block. For example, my Roam page is block 4 in my database. Running the following: window.roamAlphaAPI.pull('[*]',4)
returns a javascript object similar to the following:
:node/title: "Roam"
:block/uid: "MOa3Jbhca"
:attrs/lookup: Array(3)
0: {:db/id: 4}
1: {:db/id: 2636}
2: {:db/id: 11268}
:: Array(16)
0: {:db/id: 2471}
1: {:db/id: 3473}
2: {:db/id: 3526}
3: {:db/id: 3532}
4: {:db/id: 3550}
5: {:db/id: 11267}
6: {:db/id: 11268}
7: {:db/id: 23920}
8: {:db/id: 23921}
9: {:db/id: 23923}
10: {:db/id: 26049}
11: {:db/id: 26050}
12: {:db/id: 26054}
13: {:db/id: 30550}
14: {:db/id: 30551}
15: {:db/id: 30552}
:block/open: true
:children/view-type: ":bullet"
:create/email: "foo@bar.com"
:create/time: 1580480550337
:db/id: 4
:edit/email: "foo@bar.com"
:edit/time: 1597092215174
:entity/attrs: [Array(3)]
It’s worth highlighting the key fields and types of blocks. All blocks have the following :db/id- integer id representing id in database
:block/uid - 9 char string representing block uid, eg “MOa3Jbhca” - Prepending this with “https://roamresearch.com/#/app/YOURDBNAME/page/” will give a link to that page or block.
Blocks may have the following:
:block/children: which returns an array of blocks that are children of the target.
Pages have a special field: :node/title: which contains the page name
whereas all other blocks have a field called:
:block/string: which contains the block content as a string.
:block/refs: If a block references other blocks, then it may have this field which contains those blocks reference.
Putting “q” and “pull” together : the stupid way
So if you wanted a full representation of all pages in your database, you could run the following:
window.roamAlphaAPI.q('[
:find ?e
:where [?e :node/title] ]
')
.map(page=>
window.roamAlphaAPI.pull('[*]',page[0]
))
One line version: window.roamAlphaAPI.q('[ :find ?e :where [?e :node/title] ] ') .map(page=> window.roamAlphaAPI.pull('[*]',page[0]))
The “q” part returns an array of pages reference, each of which is fed into a “pull” expression to pull all fields. This returns an array of javascript object of pages, that we can do fun stuff with.
Putting “q” and “pull” together : Using Pull Parameters
However there is a better way of doing the above.
window.roamAlphaAPI.q('[
:find (pull ?e [*])
:where [?e :node/title]]
')
One line version: window.roamAlphaAPI.q('[ :find (pull ?e [*]) :where [?e :node/title]] ')
This uses a pull expression “(pull ?e [*])”” which is invoked for each node, and pulls all fields defined as “component fields”, which are whatever Roam has defined as fields to be pulled on each query.
Going Deeper: Pulling Children
If we drill into the output of the command above, we find something annoying. When a block points to other blocks (eg :block/children or :block/refs), we only have access to the dbid of those other blocks. For example, here is the object returned from the q/pull above showing one of my daily pages:
title: "July 27th, 2020"
uid: "07-27-2020"
children: Array(2)
0: {id: 27983}
1: {id: 28054}
What if we want to get the content of those children? We can always go back and do pulls for each block, but there is a better way:
window.roamAlphaAPI.q('[
:find (pull ?e
[* {:block/children [*]}]
)
:where [?e :node/title]]'
)
One line version: window.roamAlphaAPI.q('[ :find (pull ?e [* {:block/children [*]}]) :where [?e :node/title]]')
This section “[* {:block/children [*]}]” translated as “pull all components from block ?e, then drop down into each element of children and pull all components from that.
Looking at the output for the same page as above, it is a lot richer:
title: "July 27th, 2020"
uid: "07-27-2020"
children: Array(2)
0: {string: "[[Foo]]", email: "foo@bar.com", time: 1595804295662, refs: Array(1), children: Array(2), …}
1: {string: "#[[Quick Capture]]", email: "foo@bar.com", time: 1595838215266, refs: Array(1), uid: "RDAWLUY_g", …}
Deeper Still: Recursive Pulls
If you drill down into the content of the above, say by expanding [[Foo]], you notice something annoying. Whilst the content of Foo is populated, the contents of Foo’s children is not available.
title: "July 27th, 2020"
uid: "07-27-2020"
children: Array(4)
0:
children: Array(2)
0: {id: 27982}
1: {id: 27984}
uid: "d_H0WBRLJ"
string: "[[Foo]]"
email: "foo@bar.com"
id: 27983
1: {string: "#[[Quick Capture]]", email: "foo@bar.com", time: 1595838215266, refs: Array(1), uid: "RDAWLUY_g", …}
What we need to do is a recursive pull:
window.roamAlphaAPI.q('[
:find (pull ?e [
:node/title
:block/string
:block/children
{:block/children ...}
])
:where [?e :node/title]]')
One line version: window.roamAlphaAPI.q('[ :find (pull ?e [ :node/title :block/string :block/children {:block/children ...} ]) :where [?e :node/title]]')
The clause ” {:block/children …}” will recursively pull all children and their key fields into a javascript graph. Now if you drill down, you will find all children populated.
Query Basics
Let’s try doing some basic queries. Say we want to find all blocks in the database that reference the page called Roam. We could do something like the following:
window.roamAlphaAPI.q('[
:find (pull ?referencingBlock [*])
:in $ ?pagetitle
:where
[?referencingBlock :block/refs ?referencedPage]
[?referencedPage :node/title ?pagetitle]
]',"Roam")
One line version: window.roamAlphaAPI.q('[ :find (pull ?referencingBlock [*]) :in $ ?pagetitle :where [?referencingBlock :block/refs ?referencedPage] [?referencedPage :node/title ?pagetitle] ]',"Roam")
There is something new in this query: :in $ ?pagetitle
- this tells the api that we are going to pass in one argument to the javascript function, called ?pageTitle. In this case this is a string : “Roam”
We construct a query by describing different blocks, and their relationship to each other.
[?referencingBlock :block/refs ?referencedPage]
: we want to find all blocks that reference “referencedPage”
[?referencedPage :node/title ?pagetitle]
and referencedPage is defined as the block whose title is equal to the argument.
This is a simple example. You could take it further- eg find all children of all blocks that reference a named page.
Reverse Relationships
What if you want to relationships backwards? Say for instance you wanted a list of all pages in the database, with all blocks that referenced those pages? This is possible using the “**_**” character, as follows:
window.roamAlphaAPI.q('[
:find
(pull ?e
[:node/title {:block/_refs [:block/string] }])
:where
[?e :node/title]]')
One line version: window.roamAlphaAPI.q('[ :find (pull ?e [:node/title {:block/_refs [:block/string] }]) :where [?e :node/title]]')
This creates a “field” on each page called “_refs” which will contain an array of all blocks whose “refs” array contain this block. The title of the page and name of the linking block is pulled. This gives something like the following:
5: Array(1)
0:
title: "July 27th, 2020"
_refs: Array(2)
0: {string: "Week Starting [[July 27th, 2020]]"}
1: {string: "Mon [[July 27th, 2020]]"}
Rules
You can also build a relationship between blocks, define it as a relationship, and refer to that relationship within a query. For example:
let ancestorrule='[
[ (ancestor ?b ?a)
[?a :block/children ?b] ]
[ (ancestor ?b ?a)
[?parent :block/children ?b ]
(ancestor ?parent ?a) ] ] ]';
One line version: let ancestorrule='[ [ (ancestor ?b ?a) [?a :block/children ?b] ] [ (ancestor ?b ?a) [?parent :block/children ?b ] (ancestor ?parent ?a) ] ] ]';
This says:
?a is the ancestor of ?b IF ?b is one of ?as children (lines 2 and 3), OR
?a is the ancestor of ?b IF ?b is one of the children of some other block (called ?parent), AND
?a is the ancestor of ?parent
We can then pass this rule into the q function as follows:
window.roamAlphaAPI.q('[
:find
(pull ?block [:block/string])
:in $ ?pagetitle %
:where
[?page :node/title ?pagetitle]
(ancestor ?block ?page)
]'
,"Roam",ancestorrule);
One line version: window.roamAlphaAPI.q('[ :find (pull ?block [:block/string]) :in $ ?pagetitle % :where [?page :node/title ?pagetitle] (ancestor ?block ?page) ]' ,"Roam",ancestorrule);
Note the new variable in the section: :in $ ?pagetitle %
. The % variable refers to the rule passed as the second arg in the javascript function.
This returns the contents of all blocks that link to my “Roam” page.
Aggregate Functions
Say you wanted to run a variant on the query above which counted the number of outbound references on a page. So you want all descendants of the page block, and then to count the outbound refs from those blocks. You can do this using an aggregate function as follows:
window.roamAlphaAPI.q('[
:find (count ?refs)
:with ?e :in $ ?myid %
:where
[?e :node/title ?myid]
[?b :block/refs ?refs]
(ancestor ?b ?e)]',
"Roam", ancestorrule);
One line version: window.roamAlphaAPI.q('[ :find (count ?refs) :with ?e :in $ ?myid % :where [?e :node/title ?myid] [?b :block/refs ?refs] (ancestor ?b ?e)]', "Roam", ancestorrule);
This returns an array of arrays, containing the count.
[Array(1)]
0: Array(1)
0: 26
Datomic Reference
For further reading, see the following: https://docs.datomic.com/on-prem/pull.html https://docs.datomic.com/on-prem/query.html https://docs.datomic.com/cloud/query/query-data-reference.html
August 26, 2020
RoamPageSearch
1. Summary
Quickly jump to and interact with your Roam Research pages from anywhere on your Mac, using Alfred.
Download from packal Don’t forget to read the setup instructions below! Please make sure you are using the latest version of Alfred.
Currently supports:
- Go to daily page by hotkey/keyword
- Search and jump to page by Alfred filter
- Go to named page by hotkey/keyword
- Jump to named tag/block under daily page, by hotkey/keyword. Creates block if it does not exist
- Jump to tag/block under daily page, using dynamic filter based on a configuration page in Roam . Creates block if it does not exist. Supports multi level menu configuration.
- For 4/5 above: optionally paste clipboard under block on daily page after jumping to it
- For 4/5 above: optionally add timestamp under block on daily page after jumping to it
- For 4/5 above: optionally make TODO under block on daily page after jumping to it
- Any combinations of 6,7,8 above
- Display and jump to list of favorite pages, based on a configuration page in Roam.
- Navigate through pages in search using ⌘ and ⌥ to find outbound and inbound links
- Hit ⌘C on page name to copy bracketed page name for pasting
- Paste contents of any page into front application, using 2, 3, 10, 11 above (hold ⇧). Means you can create template pages and quickly access using Alfred.
Example use cases:
- Grab a screenshot based on a hotkey, have Alfred prompt for “work” or “home” tags, paste screenshot under that tag on daily page
- Grab selected text based on hotkey, paste under “todo” tag on daily page with timestamp and checkbox
- Use a hotkey to show a list of your favorite pages. Use another hotkey to show a list of your least favorite pages. :)
- Create template pages and paste their contents into other pages or apps. Create menus of templates, bring them up with a hot key. Bind templates to a hot key, keyword.

2 Release Notes
- 23 Aug 2020: V2.0 released.
- Added dynamic menus, paste content, timestamps, todos
- 25 Aug 2020: V3.0 released.
- Added ability to navigate through page inbound and outbound links in Alfred using ⌘ and ⌥, as well as ability to copy current page reference using ⌘-C
- 26 Aug 2020: V 3.1 released.
- Added better subtitle text.
- Fixed chrome jumping to foreground when run.
- 27 Aug 2020: V 3.3 released.
- Fixed bug that stopped links to favorites (loaded from config) from showing. Now you can have a favorite tags section on your config page, target it with a hotkey, and hold ⌘ to show all pages tagged with selected tag.
- 28 Aug 2020: V4.0 released.
- Added option to paste contents of currently visible page into front app (thanks Viktor!), giving easy fast templates accessed via dynamic menus.
- Fixed two nasty bugs: wrong chrome tab was targeted, and all page caching did not behave as expected.
- 15 Sep 2020: V 5.0 released.
- Greatly improved search: “oam” will now match “Roam” “[[Roam]]” “Foam” etc, as well as now supporting Chinese, Japanese character search. Thanks to aitsc for the PR.
- Removed caching of all page results. Too much trouble to support for very small response time gains.
- Added many keyword examples
- 16 Sep 2020: V 5.1 released.
- Fixed bug on showing linked pages
3 Setting up
3.1 Permissions
For this workflow to work, you need to enable AppleScript->Javascript in your browser of choice.
To enable AppleScript->javascript in Chrome/Brave:
View>Developer>Allow Javascript from Apple Events 
To enable AppleScript->javascript in Vivaldi: Settings->Privacy->Allow Javascript from Apple Events
To enable AppleScript->javascript in Safari:
Safari Preference>Advanced>Show Developer Menu in menu bar
Then Develop>Allow Javascript from Apple Events 
3.2 Environment Variables
The workflow has several environment variables. These can be found by clicking the [x] in Alfred, in the top left of the workflow page.


DBName: the name of your roam db. This can be found at the end of the url of your roam pages: https://roamresearch.com/#/app/YOURDBNAME . This MUST be set before running.
configPageName: the name of a page in your database where you can build dyamic Alfred menus for favorite pages and tags. See below. Defaults to “RoamSearchConfig”- I recommend you create this page as per the “Sample”RoamResearchConfig” page” section below to experiment.
keydelay: the script uses keyboard automation to add items to Alfred, as there is no data entry API as yet. This is error prone, so the script introduces delays between each step. This value represents the time in seconds between key presses. Depending on you computer, you may get strange behavior. If this happens increase this value to 0.4, 0.5 etc. Please let me know if you experience issues let me know here and I will increase the default.
Note: if your computer is set to a locale where decimals are represented as “0,3” rather than “0.3”, you will need to change this value to use “0,3”
preferredBrowser: must be either “Chrome”,“Safari”,“Vivaldi”, or “Brave”. Local webapp support does not seem possible. Firefox might be, but needs a lot of coding (PRs welcome :)
3.3 Create “RoamResearchConfig” page
If you want to use dynamic configuration (and you should!), paste the following onto a page called “RoamResearchConfig” in your Roam to run the example workflows.
<snip>
- Favorite Pages
- [[RoamSearchConfig]]
- [[Roam]]
- [[📽Projects]]
- [[[[Roam]] [[Alfred]] script]]
- Screenshot Categories
- [[Work]]
- [[Project 1]]
- [[Project 2]]
- [[Home]]
- Quick Capture Categories
- #thoughts
- [[work]]
- [[webclips]]
- [[Tech Log]]
- [[Articles]]
</snip>
4 General Usage Notes
Requires a roam tab to be open pointing to your database before it will work (script will open one on first run if not already open).
When run, the workflow will query roam dynamically. When you select an action that jumps to a page, one of two things will happen: 1) If your preferred browser has a roam tab open and active, that tab will go to that page 2) If you do not have an active roam tab open, the first roam tab you have open will be navigated to that page. The idea here is that if you are working in a tab, you can use the hotkeys and stay in that tab.
The workflow is configured with what I consider to be sensible defaults, and can be used simply by configuring hotkeys or keywords. However, all sections of the workflow that contain yellow boxes are examples and should be copied to your own workflow- these will invoke the main workflow using external triggers. This a) gives you flexibility to relate multiple hotkeys for the same use case, and b) gives you some level of protection against workflow updates breaking any custom hotkeys you create (though no promises :)
When targeting blocks under daily pages: if the named bloc does not exist, it will be created (at the top of the daily page). If multiple copies of the named bloc exist, the oldest will be targeted.
5 Example Use Cases
In general there are three flavors of usage:
Jump directly to a page. You can map hotkeys or keywords to individual pages within your database. Daily page is a special case.
Search graph by page title, and navigate through the graph from within Alfred. Navigation is restricted to either showing outbound page links within a page, and page that have links back to the current page. Selecting a page will jump to it, ⌘ and ⌥ will swap between inbound and outbound links, and ⇧will paste contents of target page into the current app.
Additionally, you can set up hotkeys/keywords to take you into the navigation at any point: eg “show me pages that contain a link to [[Favorites]]”
- Add content to daily notes page. These use cases allow adding content under tags on daily notes. There are variations to add timestamp, paste current clipboard content, and make into a todo. These can be combined.
These tags can be targeted individually by hotkey or keyword. Alternatively you can set up a page in roam containing lists of tags to target. These lists can contain a nested hierarchy that you can navigate in Alfred.
More details follow below.
5.1 Jump to daily page
The simplest feature. When invoked by hotkey or keyword, the workflow will find the first roam tab in the preferred browser, and jump to the daily page in that tab.

If you are already working in a roam tab in that browser, the script will target that tab. You can set this hotkey to ⇧^D, which is the same as Roam’s “go to daily page” key. This means it is the same key whether you are working in Roam or outside of it.
5.2 Search and jump to page by filter
When invoked by hotkey or keyword, the workflow will find the first tab pointing to Roam in the preferred browser, and use javascript to query the roamAlphaApi to get the titles of all pages. These pages will then be available for filtering in Alfred as you type.
Selecting a page will jump Roam to that page.
Holding down ⌘ and hitting enter will show pages that link to the current entry.
Holding down ⌥ and hitting enter will show outbound links to other pages from the current page.
Hitting ⌘-C will copy the current page name with braces around it. If your use case requires something else (eg markdown links) please let me know (see bottom of page).
5.3 Go to named page by hotkey/keyword
See GO TO NAMED PAGE EXAMPLE
When invoked by a hotkey or keyword, the workflow will jump directly to this page. This is useful if you have pages that you jump to a log- you can assign them hotkeys.
Set an arg which is the name of the page you want to jump to. As above, any section containing yellow blocks should ideally be moved to your own workflow.
5.4 Add Web Bookmark Example
See BOOKMARK EXAMPLE
This example runs an applescript that grabs the title and url of the front browser window, dumps it in clipboard as a markdown link, then adds it to the daily page under the heading [[webclips]].
A couple of notes about this: The applescript is scrounged from the web, and I’m unlikely to support this going forward. YMMV- it probably doesn’t have widespread browser support. I strongly recommend looking into Hook (see below) for this use case. This example shows the argument (the tag to target on daily notes) and vars (paste=true) being set. Setting paste means when we arrive at the target block on daily page, we paste the payload.

5.5 Add TODO example
See TODO EXAMPLE
This is the largely the same as the example above - it grabs selected text and sends to a [[todo]] block on daily notes. It shows the addition of extra variables “addtimestamp=true” and “todo=true”. These add timestamp and todo markers to the target block.

5.6 Hook Example
See HOOK EXAMPLE
I use this rather than the bookmark example above. This uses the versatlie Mac utility Hook to grab the title and url of the current window as a markdown link, and sends it to a tag called [[webclips]] on the daily page.
However what is neat about this is that Hook supports a LOT of apps, and this shortcut (or variations of) will work in all of them. You can use it for local files, emails, things tasks, etc. See the full list of apps hook supports here.

5.7 Grab Screenshot, with Prompt Example
See GRAB SCREENSHOT EXAMPLE
Now we get fancy. The first step in the script grabs a screenshot into the copy buffer for pasting into Roam. However instead of jumping straight to today’s daily page and pasting, it will prompt you where to put it, based on a dynamic menu generated from the page (by default) called “RoamSearchConfig”.
On my “RoamSearchConfig” page, I have the following:

I pass “Screenshot Categories” as the arg, and the config page is searched for a matching block, with the children being displayed as options:

Alfred then prompts me as follows:

If I choose “Work” or “Home” here the screenshot will be pasted under the “Work” or “Home” tag on my daily page. However if I hold down ⌥ and choose “Work”, Alfred will drop down a bullet level:

Choosing one of these jumps to or creates the appropriate bullet on Daily Pages. Note that you can pass “todo=true” “addtimestamp=true” etc to customize the behaviour.
5.8 Jump to daily page tag example
See JUMP TO DAILY TAG EXAMPLE 
This is a simplified version of the screenshot example above. It only jumps to a block on the daily page (no paste), after prompting for which block to jump to.
Note the arg (the name of the block on the config page to use as the menu root) is set in the hotkey here, and no vars are set.



5.9 Dynamic Favorite Example
See DYNAMIC FAVORITE EXAMPLE
This example queries the config page, searching for the named block passed as an arg (in this case set in the hotkey to “Favorite Pages”). Children of the “Favorite Pages” block are then searched for reference to other pages. Only one reference per child block is allowed. Nesting is not supported here.



6 External Targets
Following is more detail on the targets that are exposed by the script, and should be targeted by your own triggers. See examples above for how to call them.
You don’t need to know how these work, only that they exist, and what to pass to make them go.
6.1 Jump To Block:
Jumps to a block on the daily page, and optionally adds content under it. If the block does not exist anywhere under your current daily page, it will be created at the top of the daily page.
Pass the string content of the block to be targeted as an argument, eg generated by your hotkey, or an arg and var block. This allows the script to find the block.
The following variables can be passed into the target to control what happens when the block is jumped to:
- paste: can be set to “true” to trigger pasting of copy buffer on arrival
- todo: can be set to “true” to trigger making new target block into todo
- addtimestamp: can be set to “true” to trigger adding current timestamp to new block
These three can be combined in any combination. The various features can be fairly flexibly parameterized.
6.2 Daily Page Config
Takes one arg which is the text content of a block expected to be present on the RoamSearchConfig page. Presents child blocks of this block as menu options. Allows drilling down to child block using opinion key.
On selection, will call Jump To Block (as above), which will jump to selected block on daily page.
Any variables get passed through, allowing the functionality of paste, todo, timestamp as per Jump To Block.

6.3 Favourite Page Config
Takes one arg which is the text content of a block expected to be present on the RoamSearchConfig page. Presents child blocks of this block as menu options. Each child block is expected to link to one and only one page
On selection, linked page will be jumped to.
No vars are used used for this.

6.4 Jump To Named Page
Takes one arg, the name of a page in Roam. Jumps to this page. Takes no vars.

7 Caveats, bugs, and warnings
7.1 Use of keyboard automation
This script uses keyboard automation to insert content into Roam. This is implicitly unreliable. Holding down keys/button mashing when running actions can have strange consequences.
In particular: when using the jump to block on daily notes feature, if the block already exists the script will focus on the block, select all, then hit down arrow. This allows the block to move the the bottom of the page. However if you button mash whilst this is happening and hit a key just after the select all, you could delete all content from the block. Undo will fix it, if you notice. Blast radius is one bullet under daily notes only.
If you have strange behavior, please increase the keydelay variable, and let me know here
7.2 Browser security settings
The script requires applescript to be able to invoke javascript. You should do your own research on the implications of this.
7.3 Multiple Windows
There is some strangeness I cannot pin down on multiple windows, especially with incognito.
7.4 Support and General Issues
Please raise bug reports etc here, or post on the roam slack here
8 Development Notes
Source code is available on github
Note this repo uses osagitfilter to manage the Applescript. Instructions available here.
The code is a hot mess of hacked up applescript, javascript, and datomic. I know none of these languages well :)
August 23, 2020