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.
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.
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.
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.
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 |
A short description shown when installing or configuring a plugin.
description | Count the number of words in a note. |
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 |
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"; } }
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.
Called to insert text at a specific location in a note, when using an {expression}
.
Arguments
app
App Interface
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.
Adds options to the per-note menu, shown when editing a specific note.
Arguments
app
App Interface
noteUUID
the UUID of the note that the menu option was invoked in
Returns
Nothing
{ noteOption(app, noteUUID) { app.alert(noteUUID); } }
Called to replace some highlighted text, invoked via the selection menu.
Arguments
app
App Interface
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"; } }
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.
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"); } }
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); } }
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 noteHandle
s for all notes that match the filter parameters.
{ async insertText(app) { const noteHandles = await app.filterNotes({ tag: "daily-jots" }); return `note count: ${ noteHandles.length }`; } }
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); } }
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); } }
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"); } }
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); } }
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 noteHandle
s, 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.
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); } }
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); } }
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 noteHandle
s 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 }`; } }
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); } }
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
.
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 |
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.
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()); } }
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"); } }
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); } }
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.
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.
March 9th, 2023
insertText
changed to insertContent
, handling markdown content
insertTask
text
attribute changed to content
, handling markdown content