Write an extension
Build a small Bookmarks tool with CLI-ready commands to save and list links, plus a table that people can use in the workbench.
Before you start
You need Prompt Studio and a project. See Install Prompt Studio and Open a project.
The commands below use Bun to install packages. If you do not have Bun, use the copy of Bun inside pst: put BUN_BE_BUN=1 in front of the command and write pst instead of bun. For example, bun add @pstdio/sdk becomes BUN_BE_BUN=1 pst add @pstdio/sdk.
If extensions are new to you, read Extensions first.
1. Create the package
Create a folder for the extension, outside your project folder:
mkdir bookmarks
cd bookmarks
Add a package.json:
{
"name": "bookmarks",
"version": "0.1.0",
"displayName": "Bookmarks",
"description": "Save links for a project.",
"publisher": "acme",
"main": "./extension.ts",
"type": "module",
"engines": {
"pstdio": "^0.1.0"
}
}
publisherandnameform the extension ID, hereacme.bookmarks. Use your own publisher name. Each must start with a lowercase letter and contain only lowercase letters, numbers, and dashes.mainis the entry file.engines.pstdiois the range of extension API versions the extension works with.^0.1.0accepts every compatible release of API 0.1.pst extensions checkprints the API version of your Prompt Studio.
Then add the SDK, which has the functions and types for writing extensions:
bun add @pstdio/sdk
2. Add a command
Create extension.ts:
import { defineCommand, defineExtension, eventRef, params } from "@pstdio/sdk/extensions";
type Bookmark = { id: string; title: string; url: string };
const bookmarksChanged = eventRef<{ id: string }>({ extensionId: "acme.bookmarks", id: "changed" });
const bookmarkParams = {
title: params.text({ label: "Title", required: true }),
url: params.text({ label: "URL", required: true }),
};
const addBookmark = defineCommand({
id: "add",
title: "Add bookmark",
description: "Save a title and URL in this project's bookmarks.",
cli: true,
palette: [{ label: "Add bookmark" }],
params: bookmarkParams,
async run(ctx, { title, url }) {
const bookmark: Bookmark = { id: crypto.randomUUID(), title, url };
await ctx.storage.collection<Bookmark>("bookmarks").put(bookmark.id, bookmark);
await ctx.events.emit(bookmarksChanged, { id: bookmark.id });
return bookmark;
},
});
export default defineExtension({
commands: [addBookmark],
});
defineCommanddeclares the command. Its IDaddis local to the extension.cli: trueadds it to the command line aspst bookmarks add.paletteadds it to the dashboard’s command palette.paramsdeclares typed parameters. The dashboard builds a form from them, and the CLI turns them into--titleand--url.ctx.storageis storage that Prompt Studio keeps for this extension in the current project.- After saving, the command emits the
bookmarksChangedevent so views can refresh. The event’sextensionIdmust match<publisher>.<name>. defineExtensionlists everything the extension adds. It is the file’s default export.
The CLI is generated from this declaration; you do not write a second command-line program. An agent can read pst bookmarks add --help and supply the same title and URL as a person using the form. Return the saved bookmark so the caller receives its ID.
3. Add a view
A view shows content in the dashboard. Prompt Studio has native views for tables, boards, trees, forms, and files, and webviews for custom pages. A native table fits this tool.
Replace extension.ts with the full version:
import {
defineCommand,
defineExtension,
defineNavigationItem,
definePage,
defineView,
eventRef,
params,
workbenchModes,
} from "@pstdio/sdk/extensions";
type Bookmark = { id: string; title: string; url: string };
const bookmarksChanged = eventRef<{ id: string }>({ extensionId: "acme.bookmarks", id: "changed" });
const bookmarkParams = {
title: params.text({ label: "Title", required: true }),
url: params.text({ label: "URL", required: true }),
};
const addBookmark = defineCommand({
id: "add",
title: "Add bookmark",
description: "Save a title and URL in this project's bookmarks.",
cli: true,
palette: [{ label: "Add bookmark" }],
params: bookmarkParams,
async run(ctx, { title, url }) {
const bookmark: Bookmark = { id: crypto.randomUUID(), title, url };
await ctx.storage.collection<Bookmark>("bookmarks").put(bookmark.id, bookmark);
await ctx.events.emit(bookmarksChanged, { id: bookmark.id });
return bookmark;
},
});
const listBookmarks = defineCommand({
id: "list",
title: "List bookmarks",
description: "Read the saved bookmarks for this project.",
cli: true,
async run(ctx) {
return ctx.storage.collection<Bookmark>("bookmarks").list();
},
});
const bookmarkTable = defineView({
id: "bookmark-table",
title: "Bookmarks",
body: {
kind: "dataTable",
refreshEvents: [bookmarksChanged],
columns: [
{ id: "title", label: "Title" },
{ id: "url", label: "URL" },
],
toolbarActions: [
{
id: "add",
label: "Add bookmark",
icon: "plus",
presentation: "primary",
command: addBookmark.ref,
input: bookmarkParams,
submitLabel: "Save",
},
],
async query(ctx) {
const bookmarks = await ctx.storage.collection<Bookmark>("bookmarks").list();
return {
rows: bookmarks.map(({ id, title, url }) => ({ id, values: { title, url } })),
};
},
},
});
const bookmarksPage = definePage({
id: "bookmarks",
title: "Bookmarks",
path: "bookmarks",
icon: "bookmark",
mode: workbenchModes.project,
main: { kind: "view", view: bookmarkTable.ref, cardinality: "one" },
slots: [],
});
const bookmarksNavigation = defineNavigationItem({
id: "bookmarks",
label: "Bookmarks",
icon: "bookmark",
owner: workbenchModes.project,
action: { kind: "page", page: bookmarksPage.ref },
});
export default defineExtension({
commands: [addBookmark, listBookmarks],
views: [bookmarkTable],
pages: [bookmarksPage],
navigationItems: [bookmarksNavigation],
});
- The view’s
queryreads the saved bookmarks and returns one row for each. refreshEventsrunsqueryagain afterbookmarksChanged, so new bookmarks appear without a reload.- The toolbar action runs the same
addcommand. Itsinputopens a form for the title and URL. - The
listcommand reads the same collection as the table. It lets an agent check the result without opening the page. - The page gives the view its own address in the project, and shows it as the page’s main content.
- The navigation item adds a Bookmarks row to the project sidebar that opens the page.
4. Install it
Go to your project folder and install the extension from its folder:
cd ~/my-project
pst extensions add ../bookmarks
The path must start with ./, ../, or ~/, or be absolute. A plain name such as bookmarks installs a published extension instead.
Prompt Studio copies the folder, installs its dependencies, checks it, and turns it on for the project. The output shows the extension ID and Project: enabled for <project-id>.
Try the command:
pst bookmarks add --title "Prompt Studio" --url https://prompt.studio
pst bookmarks list
Open the dashboard and choose Bookmarks in the sidebar. The table shows the bookmark. Choose Add bookmark to save another one from the form.
Both callers use the same command and saved collection. Discover its help, or request the full execution response for an agent or script:
pst bookmarks --help
pst bookmarks add --help
pst bookmarks add --title "Extension docs" --url https://prompt.studio/docs/ --json
Without --json, a successful extension command prints its returned value as JSON. With --json, it prints the full response; check outcome.ok before using outcome.value. A failed or rejected command exits with code 1. See Make actions CLI-ready for parameter flags, aliases, and project targeting.
To install a changed version, run the same command with --force. While you work on an extension, run the watcher instead:
pst extensions dev ../bookmarks
It checks and reloads the extension each time you save a file. Keep it running while you edit, and stop it with Ctrl+C.
5. Check it
Check the installed declarations:
pst extensions check
The check reports missing references, invalid IDs, unknown icons, and features your Prompt Studio version does not support. Each problem names the extension, the contribution, and the field.
Then run the smoke test. It installs the extension into a temporary Prompt Studio, opens its pages in a real browser, and reports errors. Install the browser once, then run the test:
pst extensions install-browser
pst extensions test ../bookmarks
Exit code 0 means the pages loaded without errors. Smoke checks explains what the test covers and what it skips.
Next steps
- Let an agent build the next tool. Prompt Studio Skills, installed in every new project, includes a skill for writing extensions. Run
pst agents setup <agent-id>, then ask your agent for the tool you want. - Add pages with inspectors, editors, and custom modes with the Workbench cookbook.
- Expose the same actions to people and agents with Make actions CLI-ready.
- Check or react to commands and events with the Automation cookbook.
- Build a custom page with a webview. See Webviews and storage.
- Look up every contribution in the extension API reference, the manifest rules, and Workbench composition.
- Study complete tools in Extension Lab.