#Building an Action Pack
An action pack is a folder. Everything the assistant needs to load and run your
actions lives inside it, and installing a pack is really just dropping that
folder into the app's actions/ directory.
#Folder layout
my-pack/
info.json # metadata about the pack (required)
phrases.json # which phrases trigger which action (required)
setVolume.js # one file per action
hello.js
package.json # optional — only if you need npm dependenciesThe action files can be named whatever you like — the name just has to match the
key you use in phrases.json.
#info.json
Metadata for the whole pack. id and name are required; the rest are
recommended.
{
"name": "My Pack",
"description": "A short sentence about what this pack does.",
"id": "my-pack",
"version": "1.0.0",
"apiVersion": 1
}| Field | Required | What it's for |
|---|---|---|
id |
yes | Unique identifier. Becomes the pack's folder name once installed. |
name |
yes | Human-readable name shown in the install prompt. |
description |
no | Shown in the install prompt so users know what they're getting. |
version |
no | Your pack's version. Bump it when you ship changes. |
apiVersion |
no | The pack API your pack targets. Current packs use 1. |
Pick an id that's unlikely to collide with someone else's — it's how installs
are keyed. If a pack with the same id is already installed, the install is
treated as a conflict rather than silently overwriting.
#phrases.json
This maps each action name to a list of phrases that should trigger it. The key is the action name; the value is an array of trigger phrases.
{
"hello": ["hello", "hi", "howdy", "yo"],
"setVolume": ["set volume to {target}", "change volume to {target}"]
}Two rules connect this file to the rest of the pack:
- Each key needs a matching file. The key
setVolumerequires asetVolume.jsnext to it. If the file is missing, that action is skipped at load time. {placeholders}become parameters. Anything in curly braces is captured from the user's input and handed to your action.set volume to {target}captures whatever follows intoparams.target.
Phrase matching has a few more rules worth knowing before you write a lot of them — see Phrases & Matching.
#Action modules
Each action is a CommonJS module that exports an async run function. It
receives the captured parameters and returns a result object.
// hello.js — the simplest possible action
module.exports = {
run: async () => {
return { success: true, message: "hi :D" };
}
};#The run contract
- Signature:
run(params)—paramsis an object whose keys are the{placeholder}names from the phrase that matched. It's always safe to default it:run: async (params = {}) => { ... }. - Async:
runmay beasync/ return a Promise. The assistant awaits it. - Return value: an object with:
success— a boolean saying whether the action worked.message— the text shown back to the user in the window.
If your action throws, Personal Goober catches it and shows a generic failure
message instead, so prefer returning { success: false, message: "..." } with
something helpful when you can.
#Using parameters
Read parameters by their placeholder name. Because users phrase things differently, it's common to accept a few aliases and fall back gracefully:
// setVolume.js
const loudness = require("loudness");
module.exports = {
run: async (params = {}) => {
const target = params.target ?? params.level ?? params.value;
try {
loudness.setVolume(target);
return { success: true, message: "volume set!" };
} catch {
return { success: false, message: "i couldn't set the volume" };
}
}
};Parameters always arrive as strings (they're pulled straight from the input
text). If you need a number, a boolean, or a specific format, parse and validate
it inside run.
#Dependencies
Need an npm package? Add a package.json to your pack with its dependencies:
{
"name": "my-pack",
"version": "1.0.0",
"dependencies": {
"loudness": "^0.4.2"
}
}When the pack is installed, Personal Goober runs npm install inside the pack
folder for you. A couple of things to keep in mind:
- Dependencies are installed without running install scripts
(
--ignore-scripts), so don't rely onpostinstallhooks to set things up. - The dependency list is surfaced to the user in the install prompt, so keep it honest and minimal.
#A note on where code runs
Your run function executes in the Electron main process — full Node.js,
full access to the machine. You can read files, spawn processes, call native
modules, and reach the network. That power is the whole point, but it also means
users are trusting your pack completely. Write accordingly.
Next: Phrases & Matching to make your triggers fire reliably.