Automation cookbook
Check commands before they run, react to events after they happen, and run commands on a schedule.
Declare middleware, hooks, and schedules with their define* helpers. List the
results in the extension’s middlewares, hooks, and schedules arrays.
Callbacks receive the context first and the parameters or event payload second.
Validate a command before it runs
This example adds a project policy requiring a title on Planner ticket creation. The Planner extension must be enabled in the same project.
import { commandRef, defineExtension, defineMiddleware } from "@pstdio/sdk/extensions";
const createTicket = commandRef.forExtension({ publisher: "pstdio", name: "pstdio-planner" })<{
title?: string;
}>("create-ticket");
const requireTitle = defineMiddleware<{ title?: string }>({
id: "require-title",
command: createTicket,
run(ctx, commandParams) {
if (!commandParams.title?.trim()) {
return ctx.commands.reject({ code: "missing-title", reason: "Supply a ticket title." });
}
return ctx.commands.continue();
},
});
export default defineExtension({ middlewares: [requireTitle] });
Middleware may continue, reject, patch parameters, or replace the invocation.
It does not call a next handler. Prefer a provider’s exported command ref when
one is available so its parameter and result types stay connected to the provider.
React to lifecycle events
Use exported event refs and defineHook. Hooks observe accepted changes and
cannot reject the operation that emitted an event.
import { defineExtension, defineHook, sessionEvents, workspaceEvents } from "@pstdio/sdk/extensions";
const recordWorkspace = defineHook({
id: "record-created-workspace",
event: workspaceEvents.created,
async run(ctx, event) {
await ctx.storage.set("lastWorkspaceId", event.workspace.id);
},
});
const recordSession = defineHook({
id: "record-started-session",
event: sessionEvents.started,
async run(ctx, event) {
await ctx.storage.set("lastSessionId", event.sessionId);
},
});
export default defineExtension({ hooks: [recordWorkspace, recordSession] });
Use workspaceEvents.ready for background setup after a local workspace is ready.
The host awaits workspaceEvents.provision handlers before marking that workspace
ready; failed provisioning prevents readiness. worktreeEvents.removed observes
local worktree cleanup.
Tickets belong to the Planner extension, so core has no ticket events. To react
to ticket work, use Planner’s commands, or hook a command’s lifecycle with
commandEvent(commandRef, "completed").
Run a command on a schedule
Bind a command to a cron expression with defineSchedule:
import { defineCommand, defineExtension, defineSchedule } from "@pstdio/sdk/extensions";
const writeReport = defineCommand({
id: "write-report",
title: "Write report",
async run(ctx) {
await ctx.storage.set("lastReportAt", new Date().toISOString());
},
});
const nightlyReport = defineSchedule({
id: "nightly-report",
title: "Nightly report",
schedule: "0 2 * * *",
command: writeReport.ref,
});
export default defineExtension({ commands: [writeReport], schedules: [nightlyReport] });
The dashboard shows schedules as automations. People turn each one on or off per
project on the extension’s page in Settings → Extensions. Automations are on
by default. Set disabled: true to ship one turned off. A person’s choice always
wins over the default, and the scheduler skips automations that are off.
Use settings for project policy and storage for data that must survive a restart. See the extension API for command outcomes, schedules, and context APIs.