sfpm
Concepts

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.

OperationTimingWhen it runsNotes
buildpreAfter staging and analysis, before the builder runs.Not run for a package that the build skips.
buildpostAfter the builder and its post-build steps succeed, including the artifact metadata.Runs in sfpm build only.
installpreBefore the package is deployed or installed.Not run for a package that the org check skips.
installpostAfter 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.

StageSet by
buildsfpm build.
installsfpm install and sfpm release run.
deploysfpm deploy and sfpm deploy artifact.
poolsfpm pool fill.
validateThe PR validation workflow in the sfpm GitHub Actions.
localFallback, 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:

  • stages is a list of stage names. The hook runs only in those stages. Without it, the hook runs in every stage.
  • filter is a function that receives the hook context and returns true to 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.hooksEffect
Not set, or trueThe hook set runs.
falseThe hook set is off for this package.
{ "enabled": false }The hook set is off for this package.
Any other objectThe 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

On this page