Custom Hooks
With self-hosted GrowthBook Enterprise, you can extend GrowthBook's validation logic with Custom Hooks.
Custom Hooks are JavaScript snippets that run on the server during validation. Use them to enforce naming conventions for feature flags, check for required metadata, or add other custom validation logic.
Using Custom Hooks
To manage Custom Hooks, navigate to Settings → Custom Hooks in the GrowthBook UI.
Custom Hooks can be global or scoped to specific projects, and you can create multiple hooks of the same type for flexible, granular validation rules.
Limits
Custom Hooks are executed in a V8 Isolate, which provides a secure and efficient environment for running untrusted code. All modern JavaScript language features are supported (including async/await and fetch), but certain global objects like process.env are not available for security reasons.
The following default limits are in place to prevent abuse and ensure performance. They can all be tweaked via environment variables:
CUSTOM_HOOK_MEMORY_MB- Maximum memory allocation for the isolate (default: 32MB)CUSTOM_HOOK_CPU_TIMEOUT_MS- Maximum active CPU time (default: 100ms)CUSTOM_HOOK_WALL_TIMEOUT_MS- Maximum total run time (including async calls) (default: 5000ms)CUSTOM_HOOK_MAX_FETCH_RESP_SIZE- Maximum response size from fetch calls in bytes (default: 500KB)
Debugging
If a Custom Hook throws an error during execution, the error message will be used as the validation error shown in the UI. This allows you to provide clear feedback to users about why their changes were rejected.
When creating a Custom Hook, use the built-in test interface to tweak inputs and run the hook. The test output shows all errors, console messages, and the return value (if any).
Use console.log statements liberally while developing hooks to inspect variables and understand the flow of execution.
Incremental Changes
Each Custom Hook has an Incremental Changes Only option that affects behavior during update operations. When enabled, the hook is skipped if the same error was already present before the update.
This is especially useful for enforcing rules that may be difficult to fix retroactively. For example, if you require all features to have at least one tag, enabling this option will prevent users from being blocked by existing features that violate this rule when they attempt to make unrelated changes.
In some cases, you should NOT enable this option. For example, a hook that prevents publishing changes to "locked" features should run every time to ensure the lock is still in place.
Important: When using this option, be sure to limit each hook to a single validation check. Otherwise, a failing check early in the hook code may cause later checks to be bypassed unintentionally.
Hook Types
GrowthBook supports several Custom Hook types. Each is triggered at a different point in the validation process and receives different input parameters.
validateFeature
Called whenever a feature is about to be created or updated. Receives the full feature object as input.
Example: Require a non-empty description before the feature is toggled ON in production.
if (feature.environmentSettings.production.enabled) {
if (feature.description.trim() === "") {
throw new Error(
"Feature description is required when enabling in production."
);
}
}
Example: Require all features to have at least 1 tag.
if (feature.tags.length === 0) {
throw new Error(
"All features must have at least one tag."
);
}
Example: Don't allow empty objects as the default value for JSON features.
if (feature.valueType === "json" && feature.defaultValue === "{}") {
throw new Error(
"Default value for JSON features cannot be an empty object."
);
}
validateFeatureRevision
Called whenever a feature revision is about to be created or updated. Receives the full feature and the revision as inputs.
Example: Require a comment before publishing a draft.
if (revision.status === "published" && !revision.comment) {
throw new Error(
"A comment is required before publishing a revision."
);
}
Example: Require all percentage rollouts to use userId as the hashing attribute.
for (const env in revision.rules) {
for (const rule of revision.rules[env]) {
if (rule.type === "rollout" && rule.hashAttribute !== "userId") {
throw new Error(
"All rollouts must use 'userId' as the hash attribute."
);
}
}
}
Example: Don't allow targeting by PII (e.g. email address)
const piiAttributes = ["email", "phone", "ssn"];
for (const env in revision.rules) {
for (const rule of revision.rules[env]) {
if (rule.condition) {
for (const attr of piiAttributes) {
// `condition` is a stringified JSON object
// Look for the quoted attribute name anywhere in the string
if (rule.condition.includes(`"${attr}"`)) {
throw new Error(
`Targeting by PII (${attr}) is not allowed.`
);
}
}
}
}
}
Example: Call an external service to validate feature naming conventions.
const response = await fetch(
"https://example.com/validate-feature-name",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ featureName: feature.name }),
}
);
const result = await response.json();
if (!result.isValid) {
throw new Error(
result.message || "Feature name validation failed."
);
}
Example: If a feature has a "locked" tag, prevent publishing changes (except for one specific admin).
if (
feature.tags.includes("locked")
&& revision.status === "published"
) {
if (
revision.publishedBy?.email
!== "admin@example.com"
) {
throw new Error(
"This feature is locked and cannot be published."
);
}
}