2094 extensibility define postinstall orand preinstall function to run in application (#18037)

- add a new optional key `postInstallLogicFunctionUniversalIdentifier`
in applicationConfig
- seed postInstall function in create-twenty-app
- update execute:function options
- update doc
This commit is contained in:
martmull
2026-02-18 16:38:22 +01:00
committed by GitHub
parent 618df704e6
commit 53c314d0fa
11 changed files with 155 additions and 15 deletions
+4
View File
@@ -54,6 +54,9 @@ yarn twenty function:logs
# Execute a function with a JSON payload
yarn twenty function:execute -n my-function -p '{"key": "value"}'
# Execute the post-install function
yarn twenty function:execute --postInstall
# Uninstall the application from the current workspace
yarn twenty app:uninstall
```
@@ -63,6 +66,7 @@ yarn twenty app:uninstall
- `application-config.ts` - Application metadata configuration
- `roles/default-role.ts` - Default role for logic functions
- `logic-functions/hello-world.ts` - Example logic function with HTTP trigger
- `logic-functions/post-install.ts` - Post-install logic function (runs after app installation)
- `front-components/hello-world.tsx` - Example front component
- TypeScript configuration
- A prewired `twenty` script that delegates to the `twenty` CLI from twenty-sdk
@@ -49,6 +49,12 @@ export const copyBaseApplicationProject = async ({
fileName: 'hello-world.ts',
});
await createDefaultPostInstallFunction({
appDirectory: sourceFolderPath,
fileFolder: 'logic-functions',
fileName: 'post-install.ts',
});
await createApplicationConfig({
displayName: appDisplayName,
description: appDescription,
@@ -196,7 +202,6 @@ const handler = async (): Promise<{ message: string }> => {
return { message: 'Hello, World!' };
};
// Logic function handler - rename and implement your logic
export default defineLogicFunction({
universalIdentifier: '${universalIdentifier}',
name: 'hello-world-logic-function',
@@ -215,6 +220,38 @@ export default defineLogicFunction({
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createDefaultPostInstallFunction = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const universalIdentifier = v4();
const content = `import { defineLogicFunction } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '${universalIdentifier}';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
handler,
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createApplicationConfig = async ({
displayName,
description,
@@ -230,12 +267,14 @@ const createApplicationConfig = async ({
}) => {
const content = `import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '${v4()}',
displayName: '${displayName}',
description: '${description ?? ''}',
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
`;
@@ -53,6 +53,9 @@ yarn twenty function:logs
# Execute a function by name
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Execute the post-install function
yarn twenty function:execute --postInstall
# Uninstall the application from the current workspace
yarn twenty app:uninstall
@@ -69,7 +72,7 @@ When you run `npx create-twenty-app@latest my-twenty-app`, the scaffolder:
- Copies a minimal base application into `my-twenty-app/`
- Adds a local `twenty-sdk` dependency and Yarn 4 configuration
- Creates config files and scripts wired to the `twenty` CLI
- Generates a default application config and a default function role
- Generates a default application config, a default function role, and a post-install function
A freshly scaffolded app looks like this:
@@ -91,7 +94,8 @@ my-twenty-app/
├── roles/
│ └── default-role.ts # Default role for logic functions
├── logic-functions/
── hello-world.ts # Example logic function
── hello-world.ts # Example logic function
│ └── post-install.ts # Post-install logic function
└── front-components/
└── hello-world.tsx # Example front component
```
@@ -289,6 +293,7 @@ Every app has a single `application-config.ts` file that describes:
- **Who the app is**: identifiers, display name, and description.
- **How its functions run**: which role they use for permissions.
- **(Optional) variables**: keyvalue pairs exposed to your functions as environment variables.
- **(Optional) post-install function**: a logic function that runs after the app is installed.
Use `defineApplication()` to define your application configuration:
@@ -296,6 +301,7 @@ Use `defineApplication()` to define your application configuration:
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
@@ -311,6 +317,7 @@ export default defineApplication({
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -318,6 +325,7 @@ Notes:
- `universalIdentifier` fields are deterministic IDs you own; generate them once and keep them stable across syncs.
- `applicationVariables` become environment variables for your functions (for example, `DEFAULT_RECIPIENT_NAME` is available as `process.env.DEFAULT_RECIPIENT_NAME`).
- `defaultRoleUniversalIdentifier` must match the role file (see below).
- `postInstallLogicFunctionUniversalIdentifier` (optional) points to a logic function that runs automatically after the app is installed. See [Post-install functions](#post-install-functions).
#### Roles and permissions
@@ -450,6 +458,54 @@ Notes:
- The `triggers` array is optional. Functions without triggers can be used as utility functions called by other functions.
- You can mix multiple trigger types in a single function.
### Post-install functions
A post-install function is a logic function that runs automatically after your app is installed on a workspace. This is useful for one-time setup tasks such as seeding default data, creating initial records, or configuring workspace settings.
When you scaffold a new app with `create-twenty-app`, a post-install function is generated for you at `src/logic-functions/post-install.ts`:
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
handler,
});
```
The function is wired into your app by referencing its universal identifier in `application-config.ts`:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
You can also manually execute the post-install function at any time using the CLI:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
```
Key points:
- Post-install functions are standard logic functions — they use `defineLogicFunction()` like any other function.
- The `postInstallLogicFunctionUniversalIdentifier` field in `defineApplication()` is optional. If omitted, no function runs after installation.
- The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding.
- Post-install functions do not need triggers — they are invoked by the platform during installation or manually via `function:execute --postInstall`.
### Route trigger payload
<Warning>
+6 -2
View File
@@ -148,8 +148,9 @@ Application development commands.
- `twenty function:execute [appPath]` — Execute a logic function with a JSON payload.
- Options:
- `-n, --functionName <name>`: Name of the function to execute (required if `-u` not provided).
- `-u, --functionUniversalIdentifier <id>`: Universal ID of the function to execute (required if `-n` not provided).
- `--postInstall`: Execute the post-install logic function defined in the application config (required if `-n` and `-u` not provided).
- `-n, --functionName <name>`: Name of the function to execute (required if `--postInstall` and `-u` not provided).
- `-u, --functionUniversalIdentifier <id>`: Universal ID of the function to execute (required if `--postInstall` and `-n` not provided).
- `-p, --payload <payload>`: JSON payload to send to the function (default: `{}`).
Examples:
@@ -187,6 +188,9 @@ twenty function:execute -n my-function -p '{"name": "test"}'
# Execute a function by universal identifier
twenty function:execute -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf -p '{"key": "value"}'
# Execute the post-install function
twenty function:execute --postInstall
```
## Configuration
@@ -129,6 +129,7 @@ export const registerCommands = (program: Command): void => {
program
.command('function:execute [appPath]')
.option('--postInstall', 'Execute post-install logic function if defined')
.option(
'-p, --payload <payload>',
'JSON payload to send to the function',
@@ -147,15 +148,20 @@ export const registerCommands = (program: Command): void => {
async (
appPath?: string,
options?: {
postInstall?: boolean;
payload?: string;
functionUniversalIdentifier?: string;
functionName?: string;
},
) => {
if (!options?.functionUniversalIdentifier && !options?.functionName) {
if (
!options?.postInstall &&
!options?.functionUniversalIdentifier &&
!options?.functionName
) {
console.error(
chalk.red(
'Error: Either --functionName (-n) or --functionUniversalIdentifier (-u) is required.',
'Error: Either --postInstall or --functionName (-n) or --functionUniversalIdentifier (-u) is required.',
),
);
process.exit(1);
@@ -3,7 +3,7 @@ import { type ApiResponse } from '@/cli/utilities/api/api-response-type';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import chalk from 'chalk';
import inquirer from 'inquirer';
import { buildManifest } from '@/cli/utilities/build/manifest/manifest-build';
import { readManifestFromFile } from '@/cli/utilities/build/manifest/manifest-reader';
export class AppUninstallCommand {
private apiService = new ApiService();
@@ -25,7 +25,7 @@ export class AppUninstallCommand {
process.exit(1);
}
const { manifest } = await buildManifest(appPath);
const manifest = await readManifestFromFile(appPath);
if (!manifest) {
return { success: false, error: 'Build failed' };
@@ -3,18 +3,20 @@ import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-exec
import chalk from 'chalk';
import { type Manifest } from 'twenty-shared/application';
import { isDefined } from 'twenty-shared/utils';
import { buildManifest } from '@/cli/utilities/build/manifest/manifest-build';
import { readManifestFromFile } from '@/cli/utilities/build/manifest/manifest-reader';
export class LogicFunctionExecuteCommand {
private apiService = new ApiService();
async execute({
appPath = CURRENT_EXECUTION_DIRECTORY,
postInstall = false,
functionUniversalIdentifier,
functionName,
payload = '{}',
}: {
appPath?: string;
postInstall?: boolean;
functionUniversalIdentifier?: string;
functionName?: string;
payload?: string;
@@ -30,7 +32,7 @@ export class LogicFunctionExecuteCommand {
process.exit(1);
}
const { manifest } = await buildManifest(appPath);
const manifest = await readManifestFromFile(appPath);
if (!manifest) {
console.error(chalk.red('Failed to build manifest.'));
@@ -54,6 +56,12 @@ export class LogicFunctionExecuteCommand {
);
const targetFunction = appFunctions.find((fn) => {
if (postInstall) {
return (
fn.universalIdentifier ===
manifest.application.postInstallLogicFunctionUniversalIdentifier
);
}
if (functionUniversalIdentifier) {
return fn.universalIdentifier === functionUniversalIdentifier;
}
@@ -64,7 +72,9 @@ export class LogicFunctionExecuteCommand {
});
if (!targetFunction) {
const identifier = functionUniversalIdentifier || functionName;
const identifier = postInstall
? 'post install'
: functionUniversalIdentifier || functionName;
console.error(
chalk.red(`Function "${identifier}" not found in application.`),
);
@@ -1,7 +1,7 @@
import { ApiService } from '@/cli/utilities/api/api-service';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import chalk from 'chalk';
import { buildManifest } from '@/cli/utilities/build/manifest/manifest-build';
import { readManifestFromFile } from '@/cli/utilities/build/manifest/manifest-reader';
export class LogicFunctionLogsCommand {
private apiService = new ApiService();
@@ -16,7 +16,7 @@ export class LogicFunctionLogsCommand {
functionName?: string;
}): Promise<void> {
try {
const { manifest } = await buildManifest(appPath);
const manifest = await readManifestFromFile(appPath);
if (!manifest) {
process.exit(1);
@@ -0,0 +1,21 @@
import * as fs from 'fs-extra';
import path from 'path';
import { type Manifest, OUTPUT_DIR } from 'twenty-shared/application';
import { buildManifest } from '@/cli/utilities/build/manifest/manifest-build';
export const readManifestFromFile = async (
appPath: string,
): Promise<Manifest | null> => {
const outputDir = path.join(appPath, OUTPUT_DIR);
await fs.ensureDir(outputDir);
const manifestPath = path.join(outputDir, 'manifest.json');
if (!(await fs.pathExists(manifestPath))) {
const { manifest } = await buildManifest(appPath);
return manifest;
}
return await fs.readJson(manifestPath);
};
@@ -46,7 +46,6 @@ export default defineLogicFunction({
// databaseEventTriggerSettings: {
// eventName: 'objectName.created',
// },
],
});
`;
};
@@ -14,6 +14,7 @@ export type ApplicationMarketplaceData = {
export type ApplicationManifest = SyncableEntityOptions & {
defaultRoleUniversalIdentifier: string;
postInstallLogicFunctionUniversalIdentifier?: string;
displayName: string;
description: string;
icon?: string;