--- title: Install Hooks description: Run logic before or after the install — seed data, back up records, validate the upgrade. icon: "wrench" --- Install hooks are special logic functions that run during the install or upgrade lifecycle. They share the same handler runtime as regular [logic functions](/developers/extend/apps/logic/logic-functions) and receive an `InstallPayload` (`{ previousVersion?: string; newVersion: string }` — `previousVersion` is `undefined` on a fresh install), but they're declared with their own define functions and live outside the normal trigger model (HTTP, cron, database events). Each app may define **at most one pre-install** and **at most one post-install** function. The manifest build errors if more than one of either is detected. ``` ┌─────────────────────────────────────────────────────────────┐ │ install flow │ │ │ │ upload package → [pre-install] → metadata migration → │ │ generate SDK → [post-install] │ │ │ │ old schema visible new schema visible │ └─────────────────────────────────────────────────────────────┘ ``` ## At a glance | | `definePreInstallLogicFunction` | `definePostInstallLogicFunction` | |---|---|---| | Runs | Before the metadata migration — the **previous** schema and data are still intact | After the migration and SDK generation — the **new** schema is in place | | Execution | Always synchronous; blocks the install | Async by default (queued, 3 retries); sync opt-in via `shouldRunSynchronously: true` | | On failure | Install is **aborted** before any schema change | Async: retried up to 3 times. Sync: caller receives `POST_INSTALL_ERROR` (schema changes are **not** rolled back) | | Typical use | Back up or fix data a migration would lose; refuse a risky upgrade by throwing | Seed default data, configure the workspace, register external resources | **Rule of thumb:** default to post-install. Only reach for pre-install when the migration itself is destructive and you need to intercept the previous state before it is gone. | You want to... | Use | |---|---| | Seed data, configure the workspace, register external resources | `post-install` | | Long-running work that shouldn't block the install response | `post-install` (default async mode, with worker retries) | | Fast setup the caller relies on immediately after the install returns | `post-install` with `shouldRunSynchronously: true` | | Read or back up data that the upcoming migration would lose | `pre-install` | | Reject an upgrade that would corrupt existing data | `pre-install` (throw from the handler) | | Reconciliation on every upgrade | Either hook with `shouldRunOnVersionUpgrade: true` | ## Behavior shared by both hooks - The config is a `defineLogicFunction` config minus the trigger settings, plus `shouldRunOnVersionUpgrade`. - **When it runs**: fresh installs only, by default. Set `shouldRunOnVersionUpgrade: true` to also run on upgrades. Use `previousVersion` / `newVersion` to branch on the upgrade path. - **Idempotency matters**: async post-install may be retried, and either hook re-runs on upgrades when `shouldRunOnVersionUpgrade` is on. - The usual logic-function environment (`APPLICATION_ID`, `APP_ACCESS_TOKEN`, `API_URL`) is injected, so you can call the Twenty API with your app's token. - The hook is attached to the application manifest automatically at build time (`preInstallLogicFunction` / `postInstallLogicFunction`) — nothing to reference in [`defineApplication()`](/developers/extend/apps/config/application). - The default `timeoutSeconds` is 300 to allow longer setup tasks like data seeding. - **Not executed in dev mode**: `yarn twenty dev` skips the install flow and syncs files directly, so hooks never run there. Trigger them manually instead: ```bash filename="Terminal" yarn twenty dev:function:exec --postInstall yarn twenty dev:function:exec --preInstall ``` Runs once your app has finished installing: metadata synchronized, SDK client generated, new schema queryable. Example — seed a default record on fresh installs: ```ts src/logic-functions/post-install.ts import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define'; import { CoreApiClient } from 'twenty-client-sdk/core'; const handler = async ({ previousVersion }: InstallPayload): Promise => { if (previousVersion) return; // fresh installs only const client = new CoreApiClient(); await client.mutation({ createPostCard: { __args: { data: { name: 'Welcome to Postcard', content: 'Your first card!' } }, id: true, }, }); }; export default definePostInstallLogicFunction({ universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210', name: 'post-install', description: 'Seeds a welcome post card after install.', timeoutSeconds: 300, shouldRunOnVersionUpgrade: false, shouldRunSynchronously: false, handler, }); ``` The `shouldRunSynchronously` flag controls the execution model: - `false` *(default)* — enqueued on the message queue (`retryLimit: 3`) and run by a worker. The install response returns as soon as the job is enqueued. **Use for long-running work** — seeding large datasets, slow third-party APIs. - `true` — executed inline during the install flow. The install request blocks until the handler finishes; a thrown error surfaces as `POST_INSTALL_ERROR` to the caller (no retries). **Use for fast, must-complete-before-response work.** The migration has already been applied at this point, so a failure does not roll back schema changes — it only surfaces the error. Runs before the metadata migration, against the **previous** schema — the right place to back up data a migration would lose, or to refuse a risky upgrade. Before executing, the server runs a purely additive "pared-down sync" that registers only the new version's pre-install function; everything else — the previous version's objects, fields, and data — is untouched when your handler runs. Pre-install is always **synchronous** and blocks the install. If the handler throws, the install is aborted before any schema change — the workspace stays on the previous version in a consistent state. This is intentional: pre-install is your last chance to refuse a risky upgrade. Example — copy a legacy field's values before the migration drops it: ```ts src/logic-functions/pre-install.ts import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define'; import { CoreApiClient } from 'twenty-client-sdk/core'; const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise => { // Only the 1.x → 2.x upgrade drops the legacy `notes` field. if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) { return; } const client = new CoreApiClient(); const { postCards } = await client.query({ postCards: { __args: { filter: { notes: { isNot: null } } }, edges: { node: { id: true, notes: true } }, }, }); // Copy legacy `notes` into `description` before the migration drops the // column. If this fails, the upgrade aborts and the workspace stays on v1. for (const { node } of postCards.edges) { await client.mutation({ updatePostCard: { __args: { id: node.id, data: { description: node.notes } }, id: true, }, }); } }; export default definePreInstallLogicFunction({ universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab', name: 'pre-install', description: 'Backs up legacy notes into description before the v2 migration.', timeoutSeconds: 300, shouldRunOnVersionUpgrade: true, handler, }); ```