Jump to content

Search Apple Notes in High Sierra and other macOS versions

Recommended Posts

Hi, I've created a workflow to find and open Apple/iCloud notes in Notes.app. Just type n[part of note title] and press enter.


The default search method is for High Sierra, but AppleScript options are also supplied that should work on other macOS versions.


Get it on Packal and feel free to open issues/make pull requests on GitHub.


Share this post

Link to post
Posted (edited)

Thanks for putting this together! I came across a bug in getting this to work, and in fixing it wound up making some modifications ...


Mainly I figured out how to extract the note contents from the sqlite db and add them to the JSON match field. The contents are gzipped in the db so I had to decompress and clean them up in order to use them for search matching ... I imagine performance could be a concern if you have a gajillion notes, but it runs very fast for me, pulling about 1000 notes down at a time.


Modified python file contents are below - I made some other changes that may or may not interest you, but I can do a PR if you like.


import sqlite3
import json
import zlib
import re

# Open notes database
home = '/'.join(__file__.split('/')[:3])
conn = sqlite3.connect(
    home + '/Library/Group Containers/group.com.apple.notes/NoteStore.sqlite')
c = conn.cursor()

# Get uuid string required in full id
c.execute("SELECT z_uuid FROM z_metadata")
uuid = str(c.fetchone()[0])

# Get tuples of note title, folder code, snippet, modification date, & id#
# 432 is the zfolder id for 'Recently Deleted'
c.execute("""SELECT t1.ztitle1,t1.zfolder,t1.zsnippet,
FROM ziccloudsyncingobject AS t1
INNER JOIN zicnotedata AS t2
ON t1.znotedata = t2.z_pk
WHERE t1.ztitle1 IS NOT NULL AND t1.zfolder IS NOT 432 AND t1.zmarkedfordeletion IS NOT 1""")
matches = c.fetchall()
# Sort by title
matches = sorted(matches, key=lambda m: m[0], reverse=False)

# Get ordered lists of folder codes and folder names
c.execute("""SELECT z_pk,ztitle2
FROM ziccloudsyncingobject
WHERE ztitle2 IS NOT NULL AND zmarkedfordeletion IS NOT 1""")
folderCodes, folderNames = zip(*c.fetchall())


# Alfred results: title = note title, arg = id to pass on, subtitle = folder name, match = the note contents
items = [{"title": m[0],
          "arg": "x-coredata://" + uuid + "/ICNote/p" + str(m[4]),
          "subtitle": folderNames[folderCodes.index(m[1])],
          #  + ("  |  " + m[2] if type(m[2]) is unicode and len(m[2]) > 0 else ""),
          #  decompress gzipped notes from the sqlite database, strip out gobbledygook footers.
          "match": zlib.decompress(m[6], 16+zlib.MAX_WBITS).split('\x1a\x10', 1)[0]}
         for m in matches]

# Do further clean up and additions to the match and subtitle fields.
for i, item in enumerate(items):
    # strip weird characters, title & weird header artifacts,
    # replace line breaks with spaces.
    txt = re.sub('^  ', '', re.sub('\n', ' ', re.sub('^.*\n', ' ', re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\xff]', '', items[i]['match']))))
    items[i]['match'] = items[i]['title'] + " " + items[i]['subtitle'] + " " + txt
    items[i]['subtitle'] += "  |  " + txt[:100]

# Custom icons for folder names that start with corresponding emoji
icons = [u'\ud83d\udcd3', u'\ud83d\udcd5',
         u'\ud83d\udcd7', u'\ud83d\udcd8', u'\ud83d\udcd9']
for i in items:
    if any(x in i['subtitle'] for x in icons):
        subtitle = i['subtitle']
        icon = subtitle[:2]
        i['subtitle'] = subtitle[3:]
        i['icon'] = {'type': 'image', 'path': 'icons/' +
                     icon.encode('raw_unicode_escape') + '.png'}

output = {"items": items}
print json.dumps(output, sort_keys=True, indent=4, separators=(',', ': '))


Edited by shortbread

Share this post

Link to post

Thanks for your contribution, this is great! @Jasondm007 was requesting this feature. Seems like people are into the full text search so we could make that the default and "search titles only" as an option.


I'm on vacation for a week but will add this when I'm back. You can make a PR if you want and/or I'll make a list of contributors on the github page. 

Share this post

Link to post

Any plans to add the option to create a new note with this workflow?  Maybe as a catchall option after any relevant results?  I might give it a go this weekend.

Share this post

Link to post
Posted (edited)

macOS 10.13.6



FYI, I'm getting:

[2018-08-09 13:55:48][input.scriptfilter] Queuing argument '(null)'

[2018-08-09 13:55:48][input.scriptfilter] Script with argument '(null)' finished

[2018-08-09 13:55:48][ERROR: input.scriptfilter] Code 1: Traceback (most recent call last):

  File "/Users/wkoutre/Library/Application Support/Alfred 3/Alfred.alfredpreferences/workflows/user.workflow.<key>/searchNotes.py", line 35, in <module>

    for m in matches]

TypeError: 'NoneType' object has no attribute '__getitem__'


Will look into it when I get a chance, just thought I'd share in the meantime.

Edited by wkoutre
Added pic and macOS version

Share this post

Link to post
On 8/2/2018 at 4:27 PM, MaxPetretta said:

Any plans to add the option to create a new note with this workflow?  Maybe as a catchall option after any relevant results?  I might give it a go this weekend.


That's a good idea, I'll put it on the to-do list!


@wkoutre and @40-02 this bug is now fixed by the PR from @shortbread  and an extra check I added. Packal and Github releases have been updated.

Share this post

Link to post

Updated the workflow with some better AppleScript methods, including one that searches note bodies. @deanishe the JSON is being output properly now using Foundation framework methods. Note bodies are obtained from AppleScript in HTML which I parse using regex (against all internet advice, but has been working so far—notes with HTML snippets are going to be less searchable). Let me know if there are better ways to grab the body text.


I tried using JXA but found it was slower for me than AppleScript where I'm careful to access large objects as references.


I looked into the new note fallback option but this seems difficult for script filters in Alfred, especially when Alfred handles narrowing the search results.


Share this post

Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now