Custom Hooks
With self-hosted GrowthBook Enterprise, you can extend GrowthBook's validation logic with Custom Hooks.
Custom Hooks are javascript snippets that are evaluated on the server during validation. For example, you can use this to enforce specific naming conventions for feature flags or to check for the presence of required metadata.
Using Custom Hooks
To manage Custom Hooks, navigate to Settings > Custom Hooks in the GrowthBook UI.
Custom Hooks can either be global or scoped to specific projects and you can create multiple hooks of the same type. This allows for flexible and 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, there is a built-in test interface where you can tweak the inputs. When you run the test, we will show all errors, console messages, and the return value (if any).
We recommend making heavy use of console.log statements while developing your hooks. This can help you inspect variables and understand the flow of execution.
Hook Types
GrowthBook supports a few different Custom Hooks. Each hook type is triggered at a different point in the validation process and has different input parameters.
validateFeature
This hook is called whenever a feature is about to be created or updated. It 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
This hook is called whenever a feature revision is about to be created or updated. It 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 percentage rollouts must use 'userId' as the hashing 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 attribute '${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.");
}
}