Lifecycle Scripts
Forge apps support install and uninstall lifecycle scripts that run automatically during key moments in the app's lifecycle. These scripts allow apps to prepare the system during installation and clean up when removed.
Overview
| Script | Trigger | Purpose |
|---|---|---|
| Install | Every time a version is deployed (dev or live) | Ensure the system is prepared for the app |
| Uninstall | When a version is revoked or the app is deleted | Clean up and restore the system to its previous state |
Defining Scripts
Scripts are declared in the app's manifest.json as relative file paths within the bundle:
{
"name": "my-app",
"version": "1.0.0",
"scripts": {
"install": "scripts/install.js",
"uninstall": "scripts/uninstall.js"
}
}
Both fields are optional — an app can define neither, one, or both.
Bundle Structure
Install Scripts
Install scripts run every time a version is deployed to either the dev or live environment. This includes:
- First-time deployment of a version
- Redeploying after a version update
- Explicit reinstall (via the
?reinstall=parameter)
Idempotency Requirement
Install scripts MUST be idempotent
Because install scripts run on every deployment — not just the first time — they must be safe to execute repeatedly. Always check if resources already exist before creating them, and use upsert patterns where possible.
Do:
- Check if a component exists before creating it
- Use upsert or "create if not exists" logic
- Set configuration values unconditionally (overwriting is fine)
Don't:
- Assume the script has never run before
- Create duplicate resources on repeated runs
- Rely on the script running only once
When Install Runs
| Event | Install runs? | Uninstall runs? |
|---|---|---|
| Deploy a new version (dev or live) | ✅ | ❌ |
| Update to a newer version | ✅ (new version) | ❌ |
| Reinstall current version | ✅ | ❌ |
| Revoke a deployed version | ❌ | ✅ |
| Delete the app | ❌ | ✅ |
Version updates never run uninstall
When deploying a new version over an existing one, only the new version's install script runs. The old version's uninstall script is not executed. This means install scripts should be written to handle upgrading from any previous state.
Error Handling
If an install script fails, the deployment is blocked — the version will not be activated. The app's status is set to failed with the error message stored for diagnostics. You can fix the script and redeploy, or trigger a reinstall.
If the install script file referenced in the manifest does not exist in the bundle, the deployment continues gracefully (treated as no script defined).
Uninstall Scripts
Uninstall scripts run when a deployed version is removed from the system. Their purpose is to clean up resources created by the install script and restore the system to its state before the app was installed.
When Uninstall Runs
- Revoking a version — When a dev or live deployment is explicitly revoked (set to null)
- Deleting the app — Uninstall runs for all active versions (both dev and live) before the app is removed
Error Handling
Uninstall failures are non-fatal
Unlike install scripts, uninstall script failures do not block the operation. If the uninstall script fails, the error is logged but the app is still removed. This ensures apps can always be deleted even if their cleanup logic has issues.
Best Practices
- Remove or deactivate resources created during install (extensions, fields, etc.)
- Deactivate rather than delete user data when possible
- Handle the case where resources were already manually removed
- Keep cleanup logic simple and resilient to partial states
Execution Environment
Scripts execute in a secure sandbox with a 60-second timeout and 64MB memory limit. The following libraries and services are available:
Available Libraries
| Library | Global Name | Description |
|---|---|---|
| Protokol SDK | $sdk |
Full platform SDK (authenticated) |
| HTTP Client | http |
Raw HTTP client (unauthenticated) |
| Lodash | _ |
Utility library |
| Moment | moment |
Date/time manipulation |
| Handlebars | Handlebars |
Template engine |
| Crypto | crypto |
Cryptographic operations |
| Buffer | Buffer |
Binary data handling |
| Bcrypt | bcrypt |
Password hashing |
| JWT | jwt |
JSON Web Token operations |
Context Variables
| Variable | Type | Description |
|---|---|---|
$input |
object |
Input payload passed to the script |
$env |
object |
App-specific environment variables |
$globalEnv |
object |
Project-level global environment variables |
$currentEnv |
string |
Current environment ("dev" or "live") |
Error Reporting
Scripts do not have logging capabilities. The only way to surface information is by throwing errors. If an install script throws, the deployment is blocked and the error message is displayed in the UI under the app's status_message.
// Throw a descriptive error to signal failure
throw new Error("Required component 'products' not found in project");
Tip
Use descriptive error messages — they are the only diagnostic information visible when a script fails.
Using the SDK
The Protokol SDK is the primary way to interact with the platform from lifecycle scripts. Initialize it with:
All SDK calls are automatically authenticated with service-level credentials scoped to the project.
Common SDK Operations
const platform = new ($sdk.version("0.10").Platform)();
// Find components
const items = await platform.component("orders").find({
currentPage: 1,
perPage: 10
});
// Create a record
await platform.component("settings").create({
key: "app_config",
value: { enabled: true }
});
// Update a record
await platform.component("settings").update(uuid, {
value: { enabled: true, version: "2.0" }
});
// Delete a record
await platform.component("settings").delete(uuid);
Examples
Install Script — Setting Up a Component Extension
This script adds a custom extension to an existing component during installation. It checks if the extension already exists to ensure idempotency.
const platform = new ($sdk.version("0.10").Platform)();
// Check if our extension already exists on the target component
const { settings } = await platform.component("products").settings();
const { extensions } = settings;
const extensionName = "my_app_tracking";
const existingExtension = extensions?.find(
ext => ext.name === extensionName
);
if (!existingExtension) {
// Add the extension with custom fields
await platform.component("products").installExtension({
name: extensionName,
is_active: true,
fields: [
{
key: "tracking_number",
name: "Tracking Number",
type: "string",
module: "input",
visible: true
},
{
key: "carrier",
name: "Carrier",
type: "string",
module: "select",
visible: true,
options: [
{ label: "DHL", value: "dhl" },
{ label: "FedEx", value: "fedex" },
{ label: "UPS", value: "ups" }
]
}
]
}, settings.version);
}
return { success: true };
Uninstall Script — Cleaning Up
The corresponding uninstall script removes the extension added during install.
const platform = new ($sdk.version("0.10").Platform)();
try {
// Remove the extension we added
const { settings } = await platform.component("products").settings();
await platform.component("products").deleteExtension(
"my_app_tracking",
settings.version
);
} catch (err) {
// Extension may have been manually removed already — safe to ignore
}
return { success: true };
Install Script — Seeding Default Data
This script creates default configuration records that the app needs to function.
const platform = new ($sdk.version("0.10").Platform)();
// Upsert pattern: try to find existing, create if not found
const existing = await platform.component("app_settings").find({
currentPage: 1,
perPage: 1,
filter: { app_id: "my-app" }
});
if (existing.data.length === 0) {
await platform.component("app_settings").create({
app_id: "my-app",
settings: {
notifications_enabled: true,
sync_interval: 3600,
retry_count: 3
}
});
}
return { success: true };
App Status Lifecycle
During script execution, the app transitions through the following statuses:
stateDiagram-v2
[*] --> Ready: App created
Ready --> Installing: Deploy triggers install script
Installing --> Ready: Script succeeds
Installing --> Failed: Script fails
Failed --> Installing: Redeploy / Reinstall
| Status | Description |
|---|---|
ready |
App is operational, no scripts running |
installing |
Install script is currently executing |
failed |
Install script failed — check status_message for details |
Troubleshooting
Install Script Fails on Deploy
- Check the app's
status_messagefor the error details - Review the script for syntax errors or uncaught exceptions
- Ensure all API endpoints referenced in the script exist
- Verify the script completes within the 60-second timeout
- Fix the issue and redeploy, or use the reinstall option
Script Runs But Resources Aren't Created
- Add temporary
throwstatements to narrow down where execution stops - Verify the SDK is initialized correctly:
new ($sdk.version("0.10").Platform)() - Check that component references match existing components in the project
- Ensure the script returns a value (even
return { success: true })
Uninstall Doesn't Clean Up Everything
- Remember that uninstall failures are non-fatal — the app is still removed
- Verify the cleanup targets still exist (they may have been removed manually)
- Wrap cleanup operations in try/catch blocks to handle partial states