Skip to content

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:

import { Platform } from '@ptkl/sdk'

const platform = new Platform()

For beta-only modules, import from the beta entry point:

import { Kortex } from '@ptkl/sdk/beta'

const kortex = new Kortex()

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);
import { Platform } from '@ptkl/sdk'

const platform = new Platform()

const users = await platform.users().find({
  currentPage: 1,
  perPage: 10
});

const user = await platform.users().get(userUuid);
import { Platform } from '@ptkl/sdk'

const platform = new Platform()

const result = await platform.functions().invoke("my_function", {
  param1: "value"
});
import { Platform } from '@ptkl/sdk'

const platform = new Platform()

// Interact with the workflow API
const workflows = await platform.workflow();
// Use http for calls to external services
const res = await http.get("https://api.example.com/data");
const res = await http.post("https://api.example.com/webhook", {
  event: "app_installed"
});

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

  1. Check the app's status_message for the error details
  2. Review the script for syntax errors or uncaught exceptions
  3. Ensure all API endpoints referenced in the script exist
  4. Verify the script completes within the 60-second timeout
  5. Fix the issue and redeploy, or use the reinstall option

Script Runs But Resources Aren't Created

  1. Add temporary throw statements to narrow down where execution stops
  2. Verify the SDK is imported and initialized correctly: import { Platform } from '@ptkl/sdk'; const platform = new Platform()
  3. Check that component references match existing components in the project
  4. Ensure the script returns a value (even return { success: true })

Uninstall Doesn't Clean Up Everything

  1. Remember that uninstall failures are non-fatal — the app is still removed
  2. Verify the cleanup targets still exist (they may have been removed manually)
  3. Wrap cleanup operations in try/catch blocks to handle partial states