Skip to main content

Event Handling

Events are the primary way plugins react to what happens in mahpastes.

How Events Work​

  1. Declare events you want in your manifest's events array
  2. Implement handler functions with the naming convention on_<event_name>
  3. Your handler receives event-specific data as its argument
Plugin = {
name = "Event Demo",
events = {"clip:created", "clip:deleted"},
}

function on_clip_created(clip)
log("New clip: " .. clip.filename)
end

function on_clip_deleted(clip_id)
log("Clip deleted: " .. tostring(clip_id))
end

Handler Naming Convention​

Event names map to handler function names by replacing : with _ and prefixing with on_. For app: events, the app: prefix is stripped for cleaner handler names.

EventHandler Function
app:startupon_startup()
clip:createdon_clip_created(clip)
watch:file_detectedon_watch_file_detected(data)
tag:added_to_clipon_tag_added_to_clip(data)
clip:renamedon_clip_renamed(data)

Event Reference​

App Lifecycle Events​

app:startup​

Fired when mahpastes starts and plugins are loaded.

Payload: None

function on_startup()
log("Plugin initialized!")

-- Good place for setup tasks
local count = #clips.list()
log("Current clip count: " .. count)
end

app:shutdown​

Fired when mahpastes is closing.

Payload: None

function on_shutdown()
log("Plugin shutting down, cleaning up...")

-- Perform cleanup, save state, etc.
storage.set("last_shutdown", tostring(utils.time()))
end

Clip Events​

clip:created​

Fired when a new clip is added to the library.

Payload:

FieldTypeDescription
idnumberUnique clip identifier
content_typestringMIME type (e.g., "image/png", "text/plain")
filenamestringOriginal filename
function on_clip_created(clip)
log("New clip created:")
log(" ID: " .. clip.id)
log(" File: " .. clip.filename)
log(" Type: " .. clip.content_type)

-- Example: Auto-tag images
if clip.content_type:match("^image/") then
tags.add_to_clip(get_or_create_tag("images"), clip.id)
end
end

clip:deleted​

Fired when a clip is permanently deleted.

Payload: clip_id (number)

note

Only the clip ID is provided because the clip data no longer exists at this point.

function on_clip_deleted(clip_id)
log("Clip deleted: " .. tostring(clip_id))

-- Clean up any plugin data associated with this clip
storage.delete("clip_metadata:" .. clip_id)
end

clip:archived​

Fired when a clip is moved to the archive.

Payload:

FieldTypeDescription
idnumberUnique clip identifier
function on_clip_archived(data)
log("Clip archived: " .. tostring(data.id))
storage.set("archived:" .. data.id, utils.time())
end

clip:unarchived​

Fired when a clip is restored from the archive.

Payload:

FieldTypeDescription
idnumberUnique clip identifier
function on_clip_unarchived(data)
log("Clip restored: " .. tostring(data.id))
storage.delete("archived:" .. data.id)
end

clip:renamed​

Fired when a clip is renamed.

Payload:

FieldTypeDescription
idnumberUnique clip identifier
filenamestringNew filename
function on_clip_renamed(data)
log("Clip " .. tostring(data.id) .. " renamed to " .. data.filename)
end

Watch Folder Events​

watch:file_detected​

Fired when a new file is detected in a watch folder, before import.

Payload:

FieldTypeDescription
pathstringFull path to the detected file
folder_idnumberID of the watch folder
function on_watch_file_detected(data)
log("File detected: " .. data.path)
log("From watch folder: " .. tostring(data.folder_id))
end

watch:import_complete​

Fired after a file from a watch folder has been imported as a clip.

Payload:

FieldTypeDescription
clip_idnumberID of the newly created clip
source_pathstringOriginal file path
folder_idnumberID of the watch folder
function on_watch_import_complete(data)
log("Imported clip " .. data.clip_id .. " from " .. data.source_path)

-- Example: Tag clips from specific folders
local folder_tags = storage.get("folder_tags:" .. data.folder_id)
if folder_tags then
for _, tag_id in ipairs(folder_tags) do
tags.add_to_clip(tag_id, data.clip_id)
end
end
end

Tag Events​

tag:created​

Fired when a new tag is created.

Payload:

FieldTypeDescription
idnumberUnique tag identifier
namestringTag name
colorstringHex color code (e.g., "#ff5733")
function on_tag_created(tag)
log("Tag created: " .. tag.name .. " (" .. tag.color .. ")")
end

tag:updated​

Fired when a tag's name or color is changed.

Payload: Same as tag:created

FieldTypeDescription
idnumberUnique tag identifier
namestringUpdated tag name
colorstringUpdated hex color code
function on_tag_updated(tag)
log("Tag updated: " .. tag.name)
end

tag:deleted​

Fired when a tag is deleted.

Payload: tag_id (number)

function on_tag_deleted(tag_id)
log("Tag deleted: " .. tostring(tag_id))
end

tag:added_to_clip​

Fired when a tag is applied to a clip.

Payload:

FieldTypeDescription
tag_idnumberID of the tag
clip_idnumberID of the clip
function on_tag_added_to_clip(data)
log("Tag " .. data.tag_id .. " added to clip " .. data.clip_id)
end

tag:removed_from_clip​

Fired when a tag is removed from a clip.

Payload: Same as tag:added_to_clip

FieldTypeDescription
tag_idnumberID of the tag
clip_idnumberID of the clip
function on_tag_removed_from_clip(data)
log("Tag " .. data.tag_id .. " removed from clip " .. data.clip_id)
end

Handler Timeouts​

Each event handler has a 30-second timeout. If your handler takes longer, it will be terminated and an error will be logged.

-- BAD: This will timeout
function on_clip_created(clip)
-- Don't do long-running operations synchronously
for i = 1, 1000000 do
http.get("https://slow-api.example.com/process")
end
end

-- GOOD: Keep handlers fast
function on_clip_created(clip)
-- Quick operations only
storage.set("pending:" .. clip.id, "true")
log("Queued clip for processing")
end

Error Handling​

Use pcall to safely handle errors without crashing your plugin:

function on_clip_created(clip)
local success, err = pcall(function()
-- Code that might fail
local response = http.post("https://api.example.com/notify", {
body = json.encode({clip_id = clip.id}),
})

if response.status >= 400 then
error("API returned " .. response.status)
end
end)

if not success then
log("Error processing clip: " .. tostring(err))
-- Gracefully handle the failure
storage.set("failed:" .. clip.id, tostring(err))
end
end

Plugin Error State​

If a plugin's handlers fail 3 consecutive times, the plugin is auto-disabled:

  • The plugin is disabled and marked with an error indicator in the UI
  • No further events are delivered and scheduled tasks stop
  • The error counter resets on any successful handler execution
  • The user must manually re-enable the plugin to retry

To avoid this:

  • Always use pcall for operations that might fail
  • Handle nil values defensively
  • Log errors for debugging
function on_clip_created(clip)
-- Defensive nil checking
if not clip then
log("Warning: received nil clip data")
return
end

if not clip.filename then
log("Warning: clip has no filename")
return
end

-- Safe to proceed
log("Processing: " .. clip.filename)
end

Best Practices​

Keep Handlers Fast​

Event handlers block the main thread. Keep them under 100ms.

-- BAD: Blocking network call
function on_clip_created(clip)
http.post("https://api.example.com/upload", {
body = fs.read(clip.path), -- Could be large
})
end

-- GOOD: Queue for later processing
function on_clip_created(clip)
-- Just record that processing is needed
storage.set("pending:" .. clip.id, utils.time())
log("Queued clip " .. clip.id)
end

-- Process in scheduled task (declared in Plugin table)
-- schedules = {{name = "process", interval = 60}}

function process()
local keys = storage.list()
for _, key in ipairs(keys) do
local clip_id = key:match("^pending:(%d+)$")
if clip_id then
process_clip(clip_id)
storage.delete(key)
end
end
end

Handle Nil Values​

Event data might be missing fields. Always check before accessing.

function on_clip_created(clip)
-- Safe access pattern
local filename = clip and clip.filename or "unknown"
local content_type = clip and clip.content_type or ""

log("File: " .. filename)

if content_type:match("^image/") then
-- Process image
end
end

Don't Block on Network​

Network requests can fail or be slow. Handle failures gracefully.

function on_clip_created(clip)
local success, result = pcall(function()
return http.post("https://api.example.com/webhook", {
body = json.encode({event = "clip_created", clip_id = clip.id}),
})
end)

if not success then
log("Network error: " .. tostring(result))
-- Don't fail the handler, just log and continue
return
end

if result.status >= 400 then
log("API error: " .. result.status)
end
end

Log Strategically​

Use logging for debugging, but don't spam the log.

function on_clip_created(clip)
-- Good: Informative but concise
log("Processing clip " .. clip.id .. " (" .. clip.content_type .. ")")

-- Bad: Too verbose for production
-- log("Entering on_clip_created handler")
-- log("Clip ID is: " .. clip.id)
-- log("Clip filename is: " .. clip.filename)
-- log("Clip content type is: " .. clip.content_type)
-- log("Exiting on_clip_created handler")
end

Complete Example​

Here's a plugin that demonstrates multiple event handlers:

Plugin = {
name = "Activity Logger",
version = "1.0.0",
description = "Logs all clip and tag activity",

events = {
"app:startup",
"app:shutdown",
"clip:created",
"clip:deleted",
"clip:archived",
"tag:added_to_clip",
},
}

local session_start

function on_startup()
session_start = utils.time()
storage.set("stats:sessions", (storage.get("stats:sessions") or 0) + 1)
log("Activity Logger started (session " .. storage.get("stats:sessions") .. ")")
end

function on_shutdown()
local duration = utils.time() - session_start
log("Session lasted " .. duration .. " seconds")
end

function on_clip_created(clip)
if not clip then return end

local count = (storage.get("stats:clips_created") or 0) + 1
storage.set("stats:clips_created", count)
log("Clip #" .. count .. " created: " .. (clip.filename or "unknown"))
end

function on_clip_deleted(clip_id)
local count = (storage.get("stats:clips_deleted") or 0) + 1
storage.set("stats:clips_deleted", count)
log("Clip deleted (total: " .. count .. ")")
end

function on_clip_archived(data)
if not data then return end
log("Archived clip: " .. tostring(data.id))
end

function on_tag_added_to_clip(data)
if not data then return end

local success, tag = pcall(function()
return tags.get(data.tag_id)
end)

local tag_name = success and tag and tag.name or tostring(data.tag_id)
log("Tag '" .. tag_name .. "' added to clip " .. data.clip_id)
end

Next Steps​