Lifecycle hooks
When hooks run during build, deploy, and install, how to scope them to stages and packages, and how to write a custom hook set.
Lifecycle hooks run your own code at fixed points in a package operation. A hook
set is registered in sfpm.config.ts. It runs for every package the command
processes, unless a package or stage turns it off. Hooks are the way to add work
that sfpm does not do itself, such as running a script or a check around each
package.
Operations and timing
sfpm runs hooks for two operations. Each one has a pre and a post timing.
| Operation | Timing | When it runs | Notes |
|---|---|---|---|
build | pre | After staging and analysis, before the builder runs. | Not run for a package that the build skips. |
build | post | After the builder and its post-build steps succeed, including the artifact metadata. | Runs in sfpm build only. |
install | pre | Before the package is deployed or installed. | Not run for a package that the org check skips. |
install | post | After the deploy or install succeeds and the artifact record is written. | Runs in sfpm install, sfpm deploy, and sfpm deploy artifact. |
Managed dependencies do not run hooks.
Stages
Each command sets a stage name. Hooks can be limited to specific stages.
| Stage | Set by |
|---|---|
build | sfpm build. |
install | sfpm install and sfpm release run. |
deploy | sfpm deploy and sfpm deploy artifact. |
pool | sfpm pool fill. |
validate | The PR validation workflow in the sfpm GitHub Actions. |
local | Fallback, when a caller does not pass a stage. |
The SFPM_STAGE environment variable overrides the stage name for a run. This lets
a pipeline use its own stage name. A process has one stage. Starting a second stage
in the same process fails.
Ordering
Each registration can set order:
'first'runs before every numbered hook.'last'runs after every numbered hook.- A number runs in ascending order. Lower numbers run first. The default is
0.
Hooks with the same order run in registration order. The registration order is the
order of the hooks array in sfpm.config.ts, and then the order of each set's
registrations.
Hooks run one at a time, and each one is awaited. If a hook throws, the package fails, and the hooks after it at that timing do not run.
Filters and stage limits
A registration can narrow when it runs:
stagesis a list of stage names. The hook runs only in those stages. Without it, the hook runs in every stage.filteris a function that receives the hook context and returnstrueto run.
Per-package overrides
Each package can turn a hook set off, or pass it settings, through
packageOptions.hooks. The key is the hook set's name.
{
"name": "@myorg/core",
"sfpm": {
"packageType": "source",
"packageOptions": {
"hooks": {
"profiles": false,
"scripts": {
"post": ["npm:typecheck"]
}
}
}
}
}Value in packageOptions.hooks | Effect |
|---|---|
Not set, or true | The hook set runs. |
false | The hook set is off for this package. |
{ "enabled": false } | The hook set is off for this package. |
| Any other object | The hook set runs. The object is passed to the hook as its per-package settings. |
packageOptions.skip is a separate control. It lists stages in which sfpm skips the
package entirely. A skipped package runs no hooks and no deploy or install work. It
is reported as skipped, not failed. Packages that depend on a skipped package still
run, so use this only when nothing downstream needs the skipped package.
Script hooks
scriptHooks in @b64hub/sfpm-hooks runs scripts from a package's package.json
or from a file path. Entries that start with npm: name a script in the package's
package.json. sfpm runs it with npm run, in the package folder. For build
post hooks, the folder is the staged copy in dist/.
Register the hook set once in sfpm.config.ts, then list scripts per package:
import { defineConfig } from "@b64hub/sfpm-core";
import { scriptHooks } from "@b64hub/sfpm-hooks";
export default defineConfig({
hooks: [scriptHooks({ scripts: [] })],
});The script hook runs for both build and install. A script you register therefore
runs during install as well as during build. Make sure it works in both places. For
example, an install from an artifact only has the files listed in the artifact's
files field.
Writing a custom hook set
Any project can define its own hook set. This is the same mechanism sfpm's built-in
hook sets use. A hook set is a LifecycleHooks object. It has a name and a list
of hooks. Each registration names an operation, a timing, and a handler. The
handler receives a HookContext.
import { defineConfig, type LifecycleHooks } from "@b64hub/sfpm-core";
const auditHooks: LifecycleHooks = {
name: "audit",
hooks: [
{
operation: "install",
timing: "post",
options: { stages: ["deploy", "install"] },
handler: async (context) => {
context.logger?.info(
`${context.sfpmPackage.name} done in stage ${context.stage}`,
);
},
},
],
};
export default defineConfig({
hooks: [auditHooks],
});HookContext gives the handler the package (sfpmPackage), the target org
(targetOrg), the operation, the timing, the stage, and a logger. The
LifecycleHooks and defineConfig exports are the public API for this, and the
hook set can be turned off per package with its name. This page covers the shape
only. It does not walk through writing a full hook.
Turning hooks off for a run
sfpm deploy and sfpm deploy artifact take --no-hooks. It skips every hook set
for that run. The other commands do not have this flag. Use packageOptions.skip
or per-package overrides to scope hooks in those commands.
Next
- Deploy vs install: the commands that run install hooks.
- Builds and artifacts: what the build hooks see.
- Workspace and packages: where
sfpm.config.tsfits in the workspace model.