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: ""
:create/time: 1580480550337
:db/id: 4
:edit/email: ""
: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 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:

                      :find ?e  
                      :where [?e :node/title] ]

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.

                          :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:

                      :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: "", time: 1595804295662, refs: Array(1), children: Array(2), …}
1: {string: "#[[Quick Capture]]", email: "", 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)
    children: Array(2)
        0: {id: 27982}
        1: {id: 27984}
    uid: "d_H0WBRLJ"
    string: "[[Foo]]"
    email: ""
    id: 27983
1: {string: "#[[Quick Capture]]", email: "", time: 1595838215266, refs: Array(1), uid: "RDAWLUY_g", …}

What we need to do is a recursive pull:

                    :find (pull ?e [
                        {: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:

                        :find (pull ?referencingBlock [*])  
                        :in $ ?pagetitle   
                            [?referencingBlock :block/refs ?referencedPage]
                            [?referencedPage :node/title ?pagetitle]

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:

                        (pull ?e 
                              [:node/title {:block/_refs [:block/string] }])  
                        [?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)
    title: "July 27th, 2020"
    _refs: Array(2)
        0: {string: "Week Starting [[July 27th, 2020]]"}
        1: {string: "Mon [[July 27th, 2020]]"}


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:

                            (pull ?block [:block/string])  
                        :in $ ?pagetitle % 
                            [?page :node/title ?pagetitle] 
                            (ancestor ?block ?page)

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:

                        :find (count ?refs) 
                        :with ?e :in $ ?myid % 
                            [?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.

    0: Array(1)
        0: 26

Datomic Reference

For further reading, see the following:

August 26, 2020


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:

  1. Go to daily page by hotkey/keyword
  2. Search and jump to page by Alfred filter
  3. Go to named page by hotkey/keyword
  4. Jump to named tag/block under daily page, by hotkey/keyword. Creates block if it does not exist
  5. 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.
  6. For 4/5 above: optionally paste clipboard under block on daily page after jumping to it
  7. For 4/5 above: optionally add timestamp under block on daily page after jumping to it
  8. For 4/5 above: optionally make TODO under block on daily page after jumping to it
  9. Any combinations of 6,7,8 above
  10. Display and jump to list of favorite pages, based on a configuration page in Roam.
  11. Navigate through pages in search using ⌘ and ⌥ to find outbound and inbound links
  12. Hit ⌘C on page name to copy bracketed page name for pasting
  13. 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: . 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.


  • Favorite Pages
    • [[RoamSearchConfig]]
    • [[Roam]]
    • [[📽Projects]]
    • [[[[Roam]] [[Alfred]] script]]
  • Screenshot Categories
    • [[Work]]
      • [[Project 1]]
      • [[Project 2]]
    • [[Home]]
      • [[Tech]]
      • [[Fun]]
  • Quick Capture Categories
    • #thoughts
    • [[work]]
    • [[webclips]]
      • [[Tech Log]]
      • [[Articles]]


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:

  1. Jump directly to a page. You can map hotkeys or keywords to individual pages within your database. Daily page is a special case.

  2. 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]]”

  1. 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


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