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 the app is deleted | Clean up and restore the system to its previous state |
Defining Scripts
Scripts are declared in ptkl.config.js. The ptkl forge bundle command builds
the script source and includes the compiled lifecycle scripts in the app bundle:
import path from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
export default {
name: 'my-app',
version: '1.0.0',
scripts: {
install: path.resolve(__dirname, 'src/scripts/install.ts'),
uninstall: path.resolve(__dirname, 'src/scripts/uninstall.ts'),
},
}
Both fields are optional — an app can define neither, one, or both.
Bundle Structure
my-app/
├── ptkl.config.js
├── src/
│ └── scripts/
│ ├── install.ts
│ └── uninstall.ts
├── dist/
│ └── ...
└── scripts/
├── install.js
└── uninstall.js
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
- 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 server-side sandbox with a 60-second timeout and
64MB memory limit. Lifecycle script source is bundled by ptkl forge bundle, so
write normal TypeScript or JavaScript imports in install.ts and uninstall.ts.
Because lifecycle scripts execute server-side, browser globals such as window,
document, sessionStorage, and $forge are not available.
Lifecycle scripts are not platform function, component function, workflow
expression, or Developer Console code. The $sdk global belongs to those
platform execution contexts; Forge lifecycle script source should use SDK
imports instead.
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. Import SDK modules normally in the script source:
For beta-only modules, import from the beta entry point:
All SDK calls are automatically authenticated with credentials scoped by the
app's install_permissions.
Common SDK Operations
import { Platform } from '@ptkl/sdk'
const platform = new 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.
import { Platform } from '@ptkl/sdk'
const platform = new 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.
import { Platform } from '@ptkl/sdk'
const platform = new 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.
import { Platform } from '@ptkl/sdk'
const platform = new 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 imported and initialized correctly:
import { Platform } from '@ptkl/sdk'; const platform = new 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