Amplenote plugins

Note as of March 2023: the plugin system is in the early stages of rolling out, as we continue to build the capabilities based on user feedback. If you're interested in building plugins (even if only for your own use) and would be interested in a chance to influence the plugin system, shoot a quick email to support@amplenote.com from the email address associated with your account. Feedback can be provided in the #plugin-atelier channel on our discord.



Amplenote provides support for client-side plugins that execute in the application on all platforms, allowing for enhancement of the default client behavior. A plugin is defined by a single note in a user's account - as the note is changed and updated, the plugin will be updated as well.


This document provides an overview of what plugins are capable of, and describes the available options for plugins.



linkPlugin creation

To create a plugin, you'll need a note that contains two things: a table of plugin information, and a code block containing the Javascript code of the plugin.



linkMetadata table

A plugin metadata table contains (at least) two columns: the name of the setting, and a value for the setting.

setting name

setting value

The setting name is not case sensitive. All columns are interpreted as strings.


linkname

The only required setting is the name of the plugin, which can be defined as:

name

Plugin name here

This is the name that the user will see when invoking the plugin, and will be used as a prefix in cases where the plugin defines multiple options presented to the user.


linkicon

The name of a Material Design Icon that will be used to identify the plugin. If not provided, a generic "extension" icon will be used.

icon

search


linkdescription

A short description shown when installing or configuring a plugin.

description

Count the number of words in a note.


linksetting

Defines settings that the user can provide to configure the plugin. The user will be able to supply a string for each setting when configuring the note as a plugin. When plugin code is invoked, it has access to the settings values that the user has provided. All setting values are provided as strings.

setting

API Key


This setting name can be repeated multiple times to define multiple settings.

setting

API Key

setting

Name


linkCode

The first code block in the note will be used as the plugin's code. Any subsequent code blocks will be ignored. The plugin code should define a Javascript object. Any functions defined on this object that match the name of an action will register the plugin to handle that action.

{ insertText() { return "Hello World!" } }


To define multiple actions for a single action type, make the action-named field an object. The keys should be the name of the action, with the corresponding function as the value.

{ insertText: { "one word": function() { return "hello"; }, "two words": function() { return "hello world"; } } }


When plugin code is invoked, the plugin object will be this, for example:

{ insertText() { return this._text(); }, _text() { return "hello world"; } }


The plugin object will be instantiated when the plugin is installed in the client, retaining any state until it is reloaded (e.g. due to the plugin code being changed in the source note).

{ insertText() { this._counter++; return "hello " + this._counter; }, _counter: 0, }


Plugin action functions can return promises, which will be awaited.

{ insertText() { return new Promise(function(resolve) { setTimeout(resolve, 2000); }).then(function() { return "hello world, eventually"; }); } }


The first argument passed to a plugin action function is an application interface object that can be used to access settings and call into the host application itself.


Given a plugin with the following metadata table entry:

setting

API Key

A plugin can access the setting through app.settings:

{ insertText(app) { return app.settings["API Key"]; } }


For action functions that receive arguments, the app argument will still be the first argument, before any other arguments:

{ replaceText(app, text) { return text + " more"; } }




linkActions

Plugin actions are the interaction points that define how a plugin's code gets called. Actions functions that return values can either return the value directly, or return a Promise that resolves to the value when ready. All action functions are passed an App Interface object as the first argument.


linkinsertText

Called to insert text at a specific location in a note, when using an {expression}.

Arguments

Returns

String new text to insert in the note in place of the {expression}

{ insertText(app) { return "hello world"; } }

The auto-complete menu for expression will include any installed plugins:


The plugin's insertText action will be called to replace the {Test Plugin} expression, where "Test Plugin" is the configured plugin name.


linknoteOption

Adds options to the per-note menu, shown when editing a specific note.

Arguments

noteUUID the UUID of the note that the menu option was invoked in

Returns

Nothing

{ noteOption(app, noteUUID) { app.alert(noteUUID); } }



linkreplaceText

Called to replace some highlighted text, invoked via the selection menu.

Arguments

text the String of selected text

Returns

String new text to replace the selected text with

null to cancel the replacement. Note that the replacement will also be cancelled if the user changes the selection before this action returns a value.

{ replaceText(app, text) { return "new text"; } }




linkApp Interface

The app interface provides the means of interacting with the application. It is passed to all plugin action functions as the first argument. All app interface functions should be considered asynchronous, returning a Promise that will either resolve to the result, or reject if there is an error.


Some app interface functions take a noteHandle argument. Note handles are objects that can identify notes even if they do not yet exist (e.g. future daily jot notes). Calling an app interface function with the noteHandle of a note that does not yet exist is likely to create that note, for example when inserting content into the note.


An individual note that exists in identified by a uuid - however there are some cases where a note may not yet have been acknowledged by the server (e.g. in an extended period of offline usage), in which case the note's uuid may be prefixed with the string "local-" and may change once the note is fully persisted to the Amplenote servers. The local-prefixed uuid can continue to be used on the same client that created the note, even after the note has been fully persisted.


linkapp.alert

Show the user a message. The name of the plugin is shown in the title of the dialog.

Arguments

message the String to show the user

Returns

Nothing

{ noteOption(app, noteUUID) { app.alert("This is an alert"); } }



linkapp.createNote

Create a new note, optionally specifying a name and/or tags to apply.

Arguments

name - optional String name to give the new note

tags - optional Array of String tag names to apply to the new note

Returns

uuid of the newly created note. This is typically a local-prefixed UUID that may change once persisted to the remote servers, but can continue to be used on the same client to identify the note. Calling findNote with this uuid will return a noteHandle with a non-local-prefixed UUID if the note has since completed persistence.

{ async noteOption(app, noteUUID) { const uuid = await app.createNote("some new note", [ "some-tag" ]); app.alert(uuid); } }


linkapp.filterNotes

Find noteHandles for all notes matching a set of filter criteria.

Arguments

(optional) object describing filter parameters, containing any of the following properties:

group - filter group to apply. This corresponds to the group= query string parameter when viewing https://www.amplenote.com/notes and filtering on a specific group or set of groups. Multiple groups can be specified with a , separator.

Examples


tag - tag filter to apply. This corresponds to the tag= query string parameter when viewing https://www.amplenote.com/notes and filtering on a specific tag or set of tags. Multiple tags can be specified with a , separator - matching notes must have all specified tags. A tag prefixed with ^ will only match notes that do not have the tag.

Examples

{ tag: "daily-jots" }

{ tag: "daily-jots,todo" } - matches notes that have the daily-jots tag and the todo tag.

{ tag: "daily-jots,^todo/next" } - matches notes that have the daily-jots tag and do not have the todo/next tag.

Returns

An array of noteHandles for all notes that match the filter parameters.

{ async insertText(app) { const noteHandles = await app.filterNotes({ tag: "daily-jots" }); return `note count: ${ noteHandles.length }`; } }


linkapp.findNote

Finds the noteHandle of a note, if the note is extant and not marked as deleted. In addition to verifying whether a note exists, this can be used to fill in some additional details for a note, e.g. if the plugin only has a noteUUID it can call this to get the name and tags applied to the note.

Arguments

noteHandle identifying the note to find

Returns

noteHandle of the note, or null if the note does not exist or has been marked as deleted

{ async noteOption(app, noteUUID) { const noteHandle = await app.findNote({ uuid: noteUUID }); app.alert(noteHandle.name); } }


linkapp.getNoteContent

Get the content of a note, as markdown.

Arguments

noteHandle identifying the note

Returns

The content of the note, as markdown.

{ async noteOption(app, noteUUID) { const markdown = await app.getNoteContent({ uuid: noteUUID }); app.alert(markdown); } }


linkapp.insertContent

Inserts content at the beginning of a note.

Arguments

noteHandle identifying the note to insert the text into

content: String of markdown-formatted content to insert

Returns

Nothing

{ noteOption(app, noteUUID) { app.insertContent({ uuid: noteUUID }, "this is some **bold** text"); } }


linkapp.insertTask

Inserts a new task at the beginning of a note. See also: note.insertTask.

Arguments

noteHandle identifying the note to insert the task into

task object, with the following attributes (all are optional):

content: String of markdown-formatted content to use in the task

hideUntil: Number a unix timestamp (seconds) to use for the "Hide until" time

startAt: Number a unix timestamp (seconds) to use for the "Start at" time

Returns

The UUID of the newly created task

Throws

RangeError if the provided content is not valid in a task (e.g. - a bullet list item)

{ async noteOption(app, noteUUID) { const taskUUID = await app.insertTask({ uuid: noteUUID }, { text: "this is a task" }); app.alert(taskUUID); } }

link

linkapp.notes

The notes object provides an alternative - and simpler - way to interact with specific notes. Depending on the purpose, it may be preferable than the noteHandle-based functions available on the main app interface. Functions on the notes object return Note interface objects. As with noteHandles, a note interface object may represent a note that does not (yet) exist. Calling any note interface function that requires an extant note will create the note first, if it doesn't already exist.


linkapp.notes.create

Create a new note. This is an alternative interface to app.createNote.

Arguments

name the String to use as the new note's name

tags an Array of String tag names to apply to the new note

Returns

Note interface object for the newly created note

{ async noteOption(app, noteUUID) { const note = await app.notes.create("some new note", [ "some-tag" ]); app.alert(note.uuid); } }


linkapp.notes.dailyJot

Gets a note interface for the daily jot note on the day corresponding to the given timestamp.

Arguments

timestamp unix timestamp Number (seconds) indicating any time on the day the daily jot note should be for

Returns

Note interface object for the daily jot note

{ async noteOption(app, noteUUID) { const tomorrowTimestamp = Math.floor(Date.now() / 1000) + 60 * 60 * 24; const note = await app.notes.dailyJot(tomorrowTimestamp); app.alert(note.name); } }


linkapp.notes.filter

Find noteHandles for all notes matching a set of filter criteria. This is an alternative interface to app.filterNotes.

Arguments

(optional) object describing filter parameters, containing any of the following properties:

group - filter group to apply. This corresponds to the group= query string parameter when viewing https://www.amplenote.com/notes and filtering on a specific group or set of groups. Multiple groups can be specified with a , separator.

Examples


tag - tag filter to apply. This corresponds to the tag= query string parameter when viewing https://www.amplenote.com/notes and filtering on a specific tag or set of tags. Multiple tags can be specified with a , separator - matching notes must have all specified tags. A tag prefixed with ^ will only match notes that do not have the tag.

Examples

{ tag: "daily-jots" }

{ tag: "daily-jots,todo" } - matches notes that have the daily-jots tag and the todo tag.

{ tag: "daily-jots,^todo/next" } - matches notes that have the daily-jots tag and do not have the todo/next tag.

Returns

An array of noteHandles for all notes that match the filter parameters.

{ async insertText(app) { const noteHandles = await app.notes.filter({ tag: "daily-jots" }); return `note count: ${ noteHandles.length }`; } }


linkapp.notes.find

Find a specific note. This is an alternative interface to app.findNote.

Arguments

uuid the String UUID identifying the note

Returns

Note interface object for the note, or null if the note does not exist or has been marked as deleted

{ async noteOption(app, noteUUID) { const note = await app.notes.find(noteUUID); app.alert(note.name); } }


linkapp.prompt

Show the user a message and text input, receiving the user-entered text.

{ insertText(app) { return app.prompt("Enter text").then(function(text) { return text; }); } }


If the user cancels out of the dialog, returns null.


linkapp.settings

An object containing the user-configured settings for the plugin. All values will be strings.


{ insertText(app) { return app.settings["Some setting"]; } }


This will insert the string the user has entered for "Some setting", assuming the plugin metadata table includes:

setting

Some setting



linkNote interface

The note interface provides a more convenient way to interact with the app interface functions that operate on a specific noteHandle. Note that - like a noteHandle - the note described by the note interface may or may not exist. Calling functions that modify the note content will create the note if it doesn't already exist.


linknote.content

Get the content of the note, as markdown. See also: app.getNoteContent.

Arguments

None

Returns

The content of the note, as a String of markdown

{ async noteOption(app, noteUUID) { const note = await app.notes.find(noteUUID); app.alert(await note.content()); } }


linknote.insertContent

Inserts content at the beginning of a note. See also: app.insertContent.

Arguments

content: String of markdown-formatted content to insert

Returns

Nothing

{ noteOption(app, noteUUID) { const note = await app.notes.find(noteUUID); note.insertContent("this is some **bold** text"); } }


linknote.insertTask

Inserts a new task at the beginning of a note. See also: app.insertTask.

Arguments

task object, with the following attributes (all are optional):

content: String of markdown-formatted content to use in the task

hideUntil: Number a unix timestamp (seconds) to use for the "Hide until" time

startAt: Number a unix timestamp (seconds) to use for the "Start at" time

Returns

The UUID of the newly created task

Throws

RangeError if the provided content is not valid in a task (e.g. - a bullet list item)

{ async noteOption(app, noteUUID) { const note = await app.notes.find(noteUUID); const taskUUID = await note.insertTask({ content: "this is a task" }); app.alert(taskUUID); } }



linkPlugin code execution environment

Plugin code is executed in a sandboxed iFrame, preventing direct access to the outer page/application. In the native mobile applications, plugin iFrames are loaded inside an isolated (hidden) WebView, with each plugin iFrame providing isolation from any other plugins that may be loaded.


Plugin code is executed in the user's browser on web and in the system WebView on mobile, so it's worth keeping browser compatibility in mind. There are no polyfills applied in the plugin code sandbox, nor is any processing performed on plugin code.



linkMarkdown content

Markdown content that is passed to plugins - e.g. via app.getNoteContent - matches the feature set of converting a note to markdown (via per-note menu option or export to markdown).


Note that the markdown passed by plugins to the app is very limited as of March 2023, but will continue to be expanded to eventually support all Amplenote features.



linkUpdate history

March 9th, 2023

insertText changed to insertContent, handling markdown content

insertTask text attribute changed to content, handling markdown content