1774 extensibility v1 create an exhaustive documentation readme or dedicated section in twenty contributing doc (#16751)

As title

<img width="1108" height="894" alt="image"
src="https://github.com/user-attachments/assets/e2dc7e12-72e3-4ca3-ac7b-a94de547f82a"
/>
This commit is contained in:
martmull 2025-12-22 15:19:11 +01:00 committed by GitHub
parent 50b0665c44
commit bb73cbc380
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 669 additions and 283 deletions

View file

@ -23,6 +23,7 @@ Create Twenty App is the official scaffolding CLI for building apps on top of [T
- A Twenty workspace and an API key (create one at https://app.twenty.com/settings/api-webhooks)
## Quick start
```bash
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app

View file

@ -1,6 +1,6 @@
{
"name": "create-twenty-app",
"version": "0.2.3",
"version": "0.2.4",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",

View file

@ -1,137 +1,29 @@
import js from '@eslint/js';
import typescriptEslint from '@typescript-eslint/eslint-plugin';
import typescriptParser from '@typescript-eslint/parser';
import importPlugin from 'eslint-plugin-import';
import preferArrowPlugin from 'eslint-plugin-prefer-arrow';
import prettierPlugin from 'eslint-plugin-prettier';
import unusedImportsPlugin from 'eslint-plugin-unused-imports';
import tseslint from 'typescript-eslint';
export default [
// Base JS rules
// Base JS recommended rules
js.configs.recommended,
// Global ignores
{
ignores: ['**/node_modules/**', '**/dist/**', '**/coverage/**'],
},
// TypeScript recommended rules
...tseslint.configs.recommended,
// Base config for TS/JS files
{
files: ['**/*.{js,jsx,ts,tsx}'],
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: {
prettier: prettierPlugin,
import: importPlugin,
'prefer-arrow': preferArrowPlugin,
'unused-imports': unusedImportsPlugin,
},
rules: {
// General rules (aligned with main project)
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
'no-console': [
'warn',
{ allow: ['group', 'groupCollapsed', 'groupEnd'] },
],
'no-control-regex': 0,
'no-debugger': 'error',
'no-duplicate-imports': 'error',
'no-undef': 'off',
'no-unused-vars': 'off',
// Import rules
'import/no-relative-packages': 'error',
'import/no-useless-path-segments': 'error',
'import/no-duplicates': ['error', { considerQueryString: true }],
// Prefer arrow functions
'prefer-arrow/prefer-arrow-functions': [
'error',
{
disallowPrototype: true,
singleReturnOnly: false,
classPropertiesAllowed: false,
},
],
// Unused imports
'unused-imports/no-unused-imports': 'warn',
'unused-imports/no-unused-vars': [
'warn',
{
vars: 'all',
varsIgnorePattern: '^_',
args: 'after-used',
argsIgnorePattern: '^_',
},
],
// Prettier (formatting as lint errors if you want)
'prettier/prettier': 'error',
},
},
// TypeScript-specific configuration
{
files: ['**/*.{ts,tsx}'],
languageOptions: {
parser: typescriptParser,
parserOptions: {
ecmaFeatures: {
jsx: true,
},
project: true,
tsconfigRootDir: import.meta.dirname,
},
},
plugins: {
'@typescript-eslint': typescriptEslint,
},
rules: {
// Turn off base rule and use TS-aware versions
'no-redeclare': 'off',
'@typescript-eslint/no-redeclare': 'error',
'@typescript-eslint/ban-ts-comment': 'error',
'@typescript-eslint/consistent-type-imports': [
'error',
{
prefer: 'type-imports',
fixStyle: 'inline-type-imports',
},
],
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/no-empty-interface': [
'error',
{
allowSingleExtends: true,
},
// Common TypeScript-friendly tweaks
'@typescript-eslint/no-unused-vars': [
'warn',
{ argsIgnorePattern: '^_' },
],
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-unused-vars': 'off',
},
},
// Test files (Jest)
{
files: ['**/*.spec.@(ts|tsx|js|jsx)', '**/*.test.@(ts|tsx|js|jsx)'],
languageOptions: {
globals: {
jest: true,
describe: true,
it: true,
expect: true,
beforeEach: true,
afterEach: true,
beforeAll: true,
afterAll: true,
},
},
rules: {
'@typescript-eslint/no-non-null-assertion': 'off',
'no-unused-vars': 'off', // handled by TS rule
},
},
];

View file

@ -100,7 +100,7 @@ const createDefaultServerlessFunctionRoleConfig = async ({
appDirectory: string;
defaultServerlessFunctionRoleUniversalIdentifier: string;
}) => {
const content = `import { PermissionFlag, type RoleConfig } from 'twenty-sdk';
const content = `import { type RoleConfig } from 'twenty-sdk';
export const functionRole: RoleConfig = {
universalIdentifier: '${defaultServerlessFunctionRoleUniversalIdentifier}',
@ -168,13 +168,17 @@ const createPackageJson = async ({
uninstall: 'twenty app uninstall',
help: 'twenty help',
auth: 'twenty auth login',
lint: 'eslint',
'lint-fix': 'eslint --fix',
},
dependencies: {
'twenty-sdk': '0.2.3',
'twenty-sdk': '0.2.4',
},
devDependencies: {
'@types/node': '^24.7.2',
typescript: '^5.9.3',
'@types/node': '^24.7.2',
eslint: '^9.32.0',
'typescript-eslint': '^8.50.0',
},
};

View file

@ -1,6 +1,6 @@
{
"name": "hello-world",
"version": "0.2.1",
"version": "0.2.2",
"license": "MIT",
"engines": {
"node": "^24.5.0",
@ -17,7 +17,7 @@
"auth": "twenty auth login"
},
"dependencies": {
"twenty-sdk": "0.2.2"
"twenty-sdk": "0.2.4"
},
"devDependencies": {
"@types/node": "^24.7.2"

View file

@ -12,7 +12,6 @@ export const functionRole: RoleConfig = {
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
canBeAssignedToApplications: true,
objectPermissions: [
{
objectNameSingular: 'postCard',

View file

@ -1,89 +1,8 @@
# Deprecated: twenty-cli
This package is deprecated. Please install and use twenty-sdk instead:
This package is deprecated. Please install and use [twenty-sdk](https://www.npmjs.com/package/twenty-sdk) instead:
```bash
npm uninstall twenty-cli
npm install -g twenty-sdk
```
The command name remains the same: twenty.
A command-line interface to easily scaffold, develop, and publish applications that extend Twenty CRM (now provided by twenty-sdk).
## Requirements
- yarn >= 4.9.2
- an `apiKey`. Go to `https://twenty.com/settings/api-webhooks` to generate one
## Quick example project
```bash
# Authenticate using your apiKey (CLI will prompt for your <apiKey>)
twenty auth login
# Init a new application called hello-world
twenty app init hello-world
# Go to your app
cd hello-world
# Add a serverless function to your application
twenty app add serverlessFunction
# Add a trigger to your serverless function
twenty app add trigger
# Add axios to your application
yarn add axios
# Start dev mode: automatically syncs changes to your Twenty workspace, so you can test new functions/objects instantly.
twenty app dev
# Or use one time sync (also generates SDK automatically)
twenty app sync
# List all available commands
twenty help
```
## Application Structure
Each application in this package follows the standard application structure:
```
app-name/
├── package.json
├── README.md
├── serverlessFunctions # Custom backend logic (runs on demand)
└── ...
```
## Publish your application
Applications are currently stored in twenty/packages/twenty-apps.
You can share your application with all twenty users.
```bash
# pull twenty project
git clone https://github.com/twentyhq/twenty.git
cd twenty
# create a new branch
git checkout -b feature/my-awesome-app
```
- copy your app folder into twenty/packages/twenty-apps
- commit your changes and open a pull request on https://github.com/twentyhq/twenty
```bash
git commit -m "Add new application"
git push
```
Our team reviews contributions for quality, security, and reusability before merging.
## Contributing
- see our [Hacktoberfest 2025 notion page](https://twentycrm.notion.site/Hacktoberfest-27711d8417038037a149d4638a9cc510)
- our [Discord](https://discord.gg/cx5n4Jzs57)

View file

@ -1,6 +1,6 @@
{
"name": "twenty-cli",
"version": "0.3.0",
"version": "0.3.1",
"description": "[DEPRECATED] Use twenty-sdk instead: https://www.npmjs.com/package/twenty-sdk",
"scripts": {
"build": "echo 'use npx nx build'",

View file

@ -19,5 +19,498 @@ Apps let you build and manage Twenty customizations **as code**. Instead of conf
**Coming soon:**
- Custom UI layouts and components
## Prerequisites
## Getting Started (Coming Soon)
- Node.js 24+ and Yarn 4
- A Twenty workspace and an API key (create one at https://app.twenty.com/settings/api-webhooks)
## Getting Started
Create a new app using the official scaffolder, then authenticate and start developing:
```bash filename="Terminal"
# Scaffold a new app
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Authenticate using your API key (you'll be prompted)
yarn auth
# Start dev mode: automatically syncs local changes to your workspace
yarn dev
```
From here you can:
```bash filename="Terminal"
# Add a new entity to your application (guided)
yarn create-entity
# Generate a typed Twenty client and workspace entity types
yarn generate
# Run a onetime sync (instead of watch mode)
yarn sync
# Watch your application's functions logs
yarn logs
# Uninstall the application from the current workspace
yarn uninstall
# Display commands' help
yarn help
```
See also: the CLI reference pages for [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) and [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).
## Project structure (scaffolded)
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
A freshly scaffolded app looks like this:
```text filename="my-twenty-app/"
my-twenty-app/
package.json
yarn.lock
.gitignore
.nvmrc
.yarnrc.yml
.yarn/
releases/
yarn-4.9.2.cjs
install-state.gz
eslint.config.mjs
tsconfig.json
README.md
src/
application.config.ts
role.config.ts
// your entities, actions, and other app files
```
At a high level:
- **package.json**: Declares the app name, version, engines (Node 24+, Yarn 4), and adds `twenty-sdk` plus scripts like `dev`, `sync`, `generate`, `create-entity`, `logs`, `uninstall`, and `auth` that delegate to the local `twenty` CLI.
- **.gitignore**: Ignores common artifacts such as `node_modules`, `.yarn`, `generated/` (typed client), `dist/`, `build/`, coverage folders, log files, and `.env*` files.
- **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Lock and configure the Yarn 4 toolchain used by the project.
- **.nvmrc**: Pins the Node.js version expected by the project.
- **eslint.config.mjs** and **tsconfig.json**: Provide linting and TypeScript configuration for your apps TypeScript sources.
- **README.md**: A short README in the app root with basic instructions.
- **src/**: The main place where you define your application-as-code:
- `application.config.ts`: Global configuration for your app (metadata and runtime wiring). See “Application config” below.
- `role.config.ts`: Default function role used by your serverless functions. See “Default function role” below.
- Future entities, actions/functions, and any supporting code you add.
Later commands will add more files and folders:
- `yarn generate` will create a `generated/` folder (typed Twenty client + workspace types).
- `yarn create-entity` will add entity definition files under `src/` for your custom objects.
## Authentication
The first time you run `yarn auth`, you'll be prompted for:
- API URL (defaults to http://localhost:3000 or your current workspace profile)
- API key
Your credentials are stored per-user in `~/.twenty/config.json`. You can maintain multiple profiles and switch using `--workspace <name>`.
Examples:
```bash filename="Terminal"
# Login interactively (recommended)
yarn auth
# Use a specific workspace profile
yarn auth --workspace my-custom-workspace
```
## Use the SDK resources (types & config)
The twenty-sdk provides typed building blocks you use inside your app. Below are the key pieces you'll touch most often.
### Defining objects
Custom objects are regular TypeScript classes annotated with decorators from `twenty-sdk`. They live under `src/objects/` in your app and describe both schema and behavior for records in your workspace.
Here is an example `postCard` object from the Hello World app:
```typescript
import { type Note } from '../../generated';
import {
type AddressField,
Field,
FieldType,
type FullNameField,
Object,
OnDeleteAction,
Relation,
RelationType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
enum PostCardStatus {
DRAFT = 'DRAFT',
SENT = 'SENT',
DELIVERED = 'DELIVERED',
RETURNED = 'RETURNED',
}
@Object({
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
nameSingular: 'postCard',
namePlural: 'postCards',
labelSingular: 'Post card',
labelPlural: 'Post cards',
description: ' A post card object',
icon: 'IconMail',
})
export class PostCard {
@Field({
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
type: FieldType.TEXT,
label: 'Content',
description: "Postcard's content",
icon: 'IconAbc',
})
content: string;
@Field({
universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
type: FieldType.FULL_NAME,
label: 'Recipient name',
icon: 'IconUser',
})
recipientName: FullNameField;
@Field({
universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
type: FieldType.ADDRESS,
label: 'Recipient address',
icon: 'IconHome',
})
recipientAddress: AddressField;
@Field({
universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
type: FieldType.SELECT,
label: 'Status',
icon: 'IconSend',
defaultValue: `'${PostCardStatus.DRAFT}'`,
options: [
{ value: PostCardStatus.DRAFT, label: 'Draft', position: 0, color: 'gray' },
{ value: PostCardStatus.SENT, label: 'Sent', position: 1, color: 'orange' },
{ value: PostCardStatus.DELIVERED, label: 'Delivered', position: 2, color: 'green' },
{ value: PostCardStatus.RETURNED, label: 'Returned', position: 3, color: 'orange' },
],
})
status: PostCardStatus;
@Relation({
universalIdentifier: 'c9e2b4f4-b9ad-4427-9b42-9971b785edfe',
type: RelationType.ONE_TO_MANY,
label: 'Notes',
icon: 'IconComment',
inverseSideTargetUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.note,
onDelete: OnDeleteAction.CASCADE,
})
notes: Note[];
@Field({
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
type: FieldType.DATE_TIME,
label: 'Delivered at',
icon: 'IconCheck',
isNullable: true,
defaultValue: null,
})
deliveredAt?: Date;
}
```
Key points:
- The `@Object` decorator defines the object identity and labels used across the workspace; its `universalIdentifier` must be unique and stable across deployments.
- Each `@Field` decorator defines a field on the object with a type, label, and its own stable `universalIdentifier`.
- `@Relation` wires this object to other objects (standard or custom) and controls cascade behavior with `onDelete`.
- You can scaffold new objects using `yarn create-entity`, which guides you through naming, fields, and relationships, then generates object files similar to the `postCard` example.
### Application config (application.config.ts)
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.
When you scaffold a new app, you start with a minimal config:
```typescript
import { type ApplicationConfig } from 'twenty-sdk';
const config: ApplicationConfig = {
universalIdentifier: '<generated-app-uuid>',
displayName: 'My Twenty App',
description: 'My first Twenty app',
functionRoleUniversalIdentifier: '<generated-role-uuid>',
};
export default config;
```
You can gradually extend this file as your app grows. For example, you can add an icon and application-scoped variables:
```typescript
import { type ApplicationConfig } from 'twenty-sdk';
const config: ApplicationConfig = {
universalIdentifier: '<your-app-uuid>',
displayName: 'My App',
description: 'What your app does',
icon: 'IconWorld', // Choose an icon by name
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '<uuid>',
description: 'Default recipient used by functions',
value: 'Jane Doe',
isSecret: false,
},
},
functionRoleUniversalIdentifier: '<your-role-uuid>',
};
export default config;
```
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`).
- `functionRoleUniversalIdentifier` must match the role you define in `role.config.ts` (see below).
#### Roles and permissions
Applications can define roles that encapsulate permissions on your workspaces objects and actions. The field `functionRoleUniversalIdentifier` in `application.config.ts` designates the default role used by your apps serverless functions.
- The runtime API key injected as `TWENTY_API_KEY` is derived from this default function role.
- The typed client will be restricted to the permissions granted to that role.
- Follow leastprivilege: create a dedicated role with only the permissions your functions need, then reference its universal identifier.
##### Default function role (role.config.ts)
When you scaffold a new app, the CLI also creates `src/role.config.ts`. This file exports the default role your serverless functions will use at runtime:
```typescript
import { PermissionFlag, type RoleConfig } from 'twenty-sdk';
export const functionRole: RoleConfig = {
universalIdentifier: '<generated-role-uuid>',
label: 'My Twenty App default function role',
description: 'My Twenty App default function role',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: false,
};
```
The `universalIdentifier` of this role is automatically wired into `application.config.ts` as `functionRoleUniversalIdentifier`. In other words:
- **role.config.ts** defines what the default function role can do.
- **application.config.ts** points to that role so your functions inherit its permissions.
As you move beyond the initial scaffold, you should tighten this role and make it explicit about what it can access. A more production-ready role might look closer to:
```typescript
import { PermissionFlag, type RoleConfig } from 'twenty-sdk';
export const functionRole: RoleConfig = {
universalIdentifier: '<your-role-uuid>',
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectNameSingular: 'postCard',
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
},
],
fieldPermissions: [
{
objectNameSingular: 'postCard',
fieldName: 'content',
canReadFieldValue: false,
canUpdateFieldValue: false,
},
],
permissionFlags: ['APPLICATIONS'],
};
```
Notes:
- Start from the scaffolded role, then progressively restrict it following leastprivilege.
- Replace the `objectPermissions` and `fieldPermissions` with the objects/fields your functions need.
- `permissionFlags` control access to platform-level capabilities. Keep them minimal; add only what you need.
- See a working example in the Hello World app: [`packages/twenty-apps/hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts).
### Serverless function config and entrypoint
Each function exports a main handler and a config describing its triggers. You can mix multiple trigger types.
```typescript
// src/actions/create-new-post-card.ts
import type {
FunctionConfig,
DatabaseEventPayload,
ObjectRecordCreateEvent,
CronPayload,
} from 'twenty-sdk';
import Twenty, { type Person } from '../generated';
// main handler can accept parameters from route, cron, or database events
export const main = async (
params:
| { name?: string }
| DatabaseEventPayload<ObjectRecordCreateEvent<Person>>
| CronPayload,
) => {
const client = new Twenty(); // generated typed client
const name = 'name' in params
? params.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: 'Hello world';
const result = await client.mutation({
createPostCard: {
__args: { data: { name } },
id: true,
name: true,
},
});
return result;
};
export const config: FunctionConfig = {
universalIdentifier: '<function-uuid>',
name: 'create-new-post-card',
timeoutSeconds: 2,
triggers: [
// Public HTTP route trigger '/s/post-card/create'
{
universalIdentifier: '<route-trigger-uuid>',
type: 'route',
path: '/post-card/create',
httpMethod: 'GET',
isAuthRequired: false,
},
// Cron trigger (CRON pattern)
{
universalIdentifier: '<cron-trigger-uuid>',
type: 'cron',
pattern: '0 0 1 1 *',
},
// Database event trigger
{
universalIdentifier: '<db-trigger-uuid>',
type: 'databaseEvent',
eventName: 'person.created',
},
],
};
```
Common trigger types:
- route: Exposes your function on an HTTP path and method **under the `/s/` endpoint**:
> e.g. `path: '/post-card/create',` -> call on `<APP_URL>/s/post-card/create`
- cron: Runs your function on a schedule using a CRON expression.
- databaseEvent: Runs on workspace object lifecycle events
> e.g. `person.created`
You can create new functions in two ways:
- **Scaffolded**: Run `yarn create-entity --path <custom-path>` and choose the option to add a new function. This generates a starter file under `<custom-path>` with a `main` handler and a `config` block similar to the example above.
- **Manual**: Create a new file and export `main` and `config` yourself, following the same pattern.
### Generated typed client
Run yarn generate to create a local typed client in generated/ based on your workspace schema. Use it in your functions:
```typescript
import Twenty from './generated';
const client = new Twenty();
const { me } = await client.query({ me: { id: true, displayName: true } });
```
The client is re-generated by `yarn generate`. Re-run after changing your objects and `yarn sync` or when onboarding to a new workspace.
#### Runtime credentials in serverless functions
When your function runs on Twenty, the platform injects credentials as environment variables before your code executes:
- `TWENTY_API_URL`: Base URL of the Twenty API your app targets.
- `TWENTY_API_KEY`: Shortlived key scoped to your applications default function role.
Notes:
- You do not need to pass URL or API key to the generated client. It reads `TWENTY_API_URL` and `TWENTY_API_KEY` from process.env at runtime.
- The API keys permissions are determined by the role referenced in your `application.config.ts` via `functionRoleUniversalIdentifier`. This is the default role used by serverless functions of your application.
- Applications can define roles to follow leastprivilege. Grant only the permissions your functions need, then point `functionRoleUniversalIdentifier` to that roles universal identifier.
### Hello World example
Explore a minimal, end-to-end example that demonstrates objects, functions, and multiple triggers [here](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
## Manual setup (without the scaffolder)
While we recommend using `create-twenty-app` for the best getting-started experience, you can also set up a project manually. Do not install the CLI globally. Instead, add `twenty-sdk` as a local dependency and wire scripts in your package.json:
```bash filename="Terminal"
yarn add -D twenty-sdk
```
Then add scripts like these:
```json filename="package.json"
{
"scripts": {
"auth": "twenty auth login",
"generate": "twenty app generate",
"dev": "twenty app dev",
"sync": "twenty app sync",
"uninstall": "twenty app uninstall",
"logs": "twenty app logs",
"create-entity": "twenty app add",
"help": "twenty --help"
}
}
```
Now you can run the same commands via Yarn, e.g. `yarn dev`, `yarn sync`, etc.
## Troubleshooting
- Authentication errors: run `yarn auth` and ensure your API key has the required permissions.
- Cannot connect to server: verify the API URL and that the Twenty server is reachable.
- Types or client missing/outdated: run `yarn generate` and then `yarn dev`.
- Dev mode not syncing: ensure `yarn dev` is running and that changes are not ignored by your environment.
Discord Help Channel: https://discord.com/channels/1130383047699738754/1130386664812982322

View file

@ -915,7 +915,6 @@ export type CreateRoleInput = {
canAccessAllTools?: InputMaybe<Scalars['Boolean']>;
canBeAssignedToAgents?: InputMaybe<Scalars['Boolean']>;
canBeAssignedToApiKeys?: InputMaybe<Scalars['Boolean']>;
canBeAssignedToApplications?: InputMaybe<Scalars['Boolean']>;
canBeAssignedToUsers?: InputMaybe<Scalars['Boolean']>;
canDestroyAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
canReadAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
@ -4424,7 +4423,6 @@ export type UpdateRolePayload = {
canAccessAllTools?: InputMaybe<Scalars['Boolean']>;
canBeAssignedToAgents?: InputMaybe<Scalars['Boolean']>;
canBeAssignedToApiKeys?: InputMaybe<Scalars['Boolean']>;
canBeAssignedToApplications?: InputMaybe<Scalars['Boolean']>;
canBeAssignedToUsers?: InputMaybe<Scalars['Boolean']>;
canDestroyAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
canReadAllObjectRecords?: InputMaybe<Scalars['Boolean']>;

View file

@ -898,7 +898,6 @@ export type CreateRoleInput = {
canAccessAllTools?: InputMaybe<Scalars['Boolean']>;
canBeAssignedToAgents?: InputMaybe<Scalars['Boolean']>;
canBeAssignedToApiKeys?: InputMaybe<Scalars['Boolean']>;
canBeAssignedToApplications?: InputMaybe<Scalars['Boolean']>;
canBeAssignedToUsers?: InputMaybe<Scalars['Boolean']>;
canDestroyAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
canReadAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
@ -4253,7 +4252,6 @@ export type UpdateRolePayload = {
canAccessAllTools?: InputMaybe<Scalars['Boolean']>;
canBeAssignedToAgents?: InputMaybe<Scalars['Boolean']>;
canBeAssignedToApiKeys?: InputMaybe<Scalars['Boolean']>;
canBeAssignedToApplications?: InputMaybe<Scalars['Boolean']>;
canBeAssignedToUsers?: InputMaybe<Scalars['Boolean']>;
canDestroyAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
canReadAllObjectRecords?: InputMaybe<Scalars['Boolean']>;

View file

@ -30,66 +30,152 @@ npm install twenty-sdk
yarn add twenty-sdk
```
## Getting started
You can either scaffold a new app or add the SDK to an existing one.
## Usage
- Start new (recommended):
```bash
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
```
- Existing project: install the SDK as shown above, then use the CLI below.
```
Usage: twenty [options] [command]
CLI for Twenty application development
Options:
--workspace <name> Use a specific workspace configuration (default: "default")
-V, --version output the version number
-h, --help display help for command
Commands:
auth Authentication commands
app Application development commands
help [command] display help for command
```
## Global Options
- `--workspace <name>`: Use a specific workspace configuration profile. Defaults to `default`. See Configuration for details.
## Commands
### Auth
Authenticate the CLI against your Twenty workspace.
- `twenty auth login` — Authenticate with Twenty.
- Options:
- `--api-key <key>`: API key for authentication.
- `--api-url <url>`: Twenty API URL (defaults to your current profile's value or `http://localhost:3000`).
- Behavior: Prompts for any missing values, persists them to the active workspace profile, and validates the credentials.
- `twenty auth logout` — Remove authentication credentials for the active workspace profile.
- `twenty auth status` — Print the current authentication status (API URL, masked API key, validity).
Examples:
## CLI quickstart
```bash
# Authenticate using your API key (CLI will prompt for it)
# Login interactively (recommended)
twenty auth login
# Add a new entity to your application (guided prompts)
twenty app add
# Provide values in flags
twenty auth login --api-key $TWENTY_API_KEY --api-url https://api.twenty.com
# Generate a typed Twenty client and TypeScript definitions for your workspace entities
twenty app generate
# Login interactively for a specific workspace profile
twenty auth login --workspace my-custom-workspace
# Start dev mode: automatically syncs changes to your workspace for instant testing
# Check status
twenty auth status
# Logout current profile
twenty auth logout
```
### App
Application development commands.
- `twenty app sync [appPath]` — One-time sync of the application to your Twenty workspace.
- Behavior: Compute your application's manifest and send it to your workspace to sync your application
- `twenty app dev [appPath]` — Watch and sync local application changes.
- Options:
- `-d, --debounce <ms>`: Debounce delay in milliseconds (default: `1000`).
- Behavior: Performs an initial sync, then watches the directory for changes and re-syncs after debounced edits. Press Ctrl+C to stop.
- `twenty app uninstall [appPath]` — Uninstall the application from the current workspace.
- Note: `twenty app delete` exists as a hidden alias for backward compatibility.
- `twenty app add [entityType]` — Add a new entity to your application.
- Arguments:
- `entityType`: one of `function` or `object`. If omitted, an interactive prompt is shown.
- Options:
- `--path <path>`: The path where the entity file should be created (relative to the current directory).
- Behavior:
- `object`: prompts for singular/plural names and labels, then creates a new object definition file.
- `function`: prompts for a name and scaffolds a serverless function file.
- `twenty app generate [appPath]` — Generate the typed Twenty client for your application.
- `twenty app logs [appPath]` — Stream application function logs.
- Options:
- `-u, --functionUniversalIdentifier <id>`: Only show logs for a specific function universal ID.
- `-n, --functionName <name>`: Only show logs for a specific function name.
Examples:
```bash
# Start dev mode with default debounce
twenty app dev
# Onetime sync of local changes
# Start dev mode with custom workspace profile
twenty app dev --workspace my-custom-workspace
# Dev mode with custom debounce
twenty app dev --debounce 1500
# One-time sync of the current directory
twenty app sync
# Uninstall the application from the current workspace
twenty app uninstall
# Add a new object interactively
twenty app add
# Generate client types
twenty app generate
# Watch all function logs
twenty app logs
# Watch logs for a specific function by name
twenty app logs -n my-function
```
## Usage (SDK)
```typescript
// Example: import what you need from the SDK
import { /* your exports */ } from 'twenty-sdk';
## Configuration
The CLI stores configuration per user in a JSON file:
- Location: `~/.twenty/config.json`
- Structure: Profiles keyed by workspace name. The active profile is selected with `--workspace <name>`.
Example configuration file:
```json
{
"profiles": {
"default": {
"apiUrl": "http://localhost:3000",
"apiKey": "<your-api-key>"
},
"prod": {
"apiUrl": "https://api.twenty.com",
"apiKey": "<your-api-key>"
}
}
}
```
## Publish your application
Applications are currently stored in [`twenty/packages/twenty-apps`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps).
Notes:
You can share your application with all Twenty users:
- If a profile is missing, `apiUrl` defaults to `http://localhost:3000` until set.
- `twenty auth login` writes the `apiUrl` and `apiKey` for the default profile.
- `twenty auth login --workspace custom-workspace` writes the `apiUrl` and `apiKey` for a custom `custom-workspace` profile.
```bash
# pull the Twenty project
git clone https://github.com/twentyhq/twenty.git
cd twenty
# create a new branch
git checkout -b feature/my-awesome-app
```
- Copy your app folder into `twenty/packages/twenty-apps`.
- Commit your changes and open a pull request on https://github.com/twentyhq/twenty
```bash
git commit -m "Add new application"
git push
```
Our team reviews contributions for quality, security, and reusability.
## Troubleshooting
- Auth errors: run `twenty auth login` again and ensure the API key has the required permissions.

View file

@ -1,6 +1,6 @@
{
"name": "twenty-sdk",
"version": "0.2.3",
"version": "0.2.4",
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",

View file

@ -106,7 +106,7 @@ export class AppCommand {
});
appCommand
.command('generate [outputPath]')
.command('generate [appPath]')
.description('Generate Twenty client')
.action(async (appPath?: string) => {
await this.generateCommand.execute(formatPath(appPath));

View file

@ -0,0 +1,19 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
export class RemoveCanBeAssignedToApplications1766077618558
implements MigrationInterface
{
name = 'RemoveCanBeAssignedToApplications1766077618558';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."role" DROP COLUMN "canBeAssignedToApplications"`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."role" ADD "canBeAssignedToApplications" boolean NOT NULL DEFAULT true`,
);
}
}

View file

@ -13,5 +13,4 @@ export const FLAT_ROLE_EDITABLE_PROPERTIES: (keyof FlatRole)[] = [
'canBeAssignedToUsers',
'canBeAssignedToAgents',
'canBeAssignedToApiKeys',
'canBeAssignedToApplications',
];

View file

@ -44,8 +44,6 @@ export const fromCreateRoleInputToFlatRoleToCreate = ({
canBeAssignedToUsers: createRoleInput.canBeAssignedToUsers ?? true,
canBeAssignedToAgents: createRoleInput.canBeAssignedToAgents ?? true,
canBeAssignedToApiKeys: createRoleInput.canBeAssignedToApiKeys ?? true,
canBeAssignedToApplications:
createRoleInput.canBeAssignedToApplications ?? true,
isEditable: true,
workspaceId,
createdAt: now,

View file

@ -18,7 +18,6 @@ export const fromRoleEntityToFlatRole = (role: RoleEntity): FlatRole => {
canBeAssignedToUsers: role.canBeAssignedToUsers,
canBeAssignedToAgents: role.canBeAssignedToAgents,
canBeAssignedToApiKeys: role.canBeAssignedToApiKeys,
canBeAssignedToApplications: role.canBeAssignedToApplications,
workspaceId: role.workspaceId,
createdAt: role.createdAt.toISOString(),
updatedAt: role.updatedAt.toISOString(),

View file

@ -70,9 +70,4 @@ export class CreateRoleInput {
@IsOptional()
@Field({ nullable: true })
canBeAssignedToApiKeys?: boolean;
@IsBoolean()
@IsOptional()
@Field({ nullable: true })
canBeAssignedToApplications?: boolean;
}

View file

@ -71,11 +71,6 @@ export class UpdateRolePayload {
@IsOptional()
@Field({ nullable: true })
canBeAssignedToApiKeys?: boolean;
@IsBoolean()
@IsOptional()
@Field({ nullable: true })
canBeAssignedToApplications?: boolean;
}
@InputType()

View file

@ -73,9 +73,6 @@ export class RoleEntity extends SyncableEntity implements Required<RoleEntity> {
@Column({ nullable: false, default: true })
canBeAssignedToApiKeys: boolean;
@Column({ nullable: false, default: true })
canBeAssignedToApplications: boolean;
@OneToMany(
() => RoleTargetEntity,
(roleTargets: RoleTargetEntity) => roleTargets.role,

View file

@ -24,7 +24,6 @@ export const STANDARD_FLAT_ROLE_METADATA_BUILDERS_BY_ROLE_NAME = {
canBeAssignedToUsers: true,
canBeAssignedToAgents: false,
canBeAssignedToApiKeys: true,
canBeAssignedToApplications: false,
},
}),
} satisfies {

View file

@ -20,7 +20,6 @@ export type CreateStandardRoleContext = {
canBeAssignedToUsers: boolean;
canBeAssignedToAgents: boolean;
canBeAssignedToApiKeys: boolean;
canBeAssignedToApplications: boolean;
};
export type CreateStandardRoleArgs = StandardBuilderArgs<'role'> & {
@ -43,7 +42,6 @@ export const createStandardRoleFlatMetadata = ({
canBeAssignedToUsers,
canBeAssignedToAgents,
canBeAssignedToApiKeys,
canBeAssignedToApplications,
},
workspaceId,
twentyStandardApplicationId,
@ -68,7 +66,6 @@ export const createStandardRoleFlatMetadata = ({
canBeAssignedToUsers,
canBeAssignedToAgents,
canBeAssignedToApiKeys,
canBeAssignedToApplications,
workspaceId,
applicationId: twentyStandardApplicationId,
createdAt: now,

View file

@ -15,6 +15,5 @@ export const ADMIN_ROLE: StandardRoleDefinition = {
canBeAssignedToUsers: true,
canBeAssignedToAgents: false,
canBeAssignedToApiKeys: true,
canBeAssignedToApplications: false,
applicationId: null, // TODO: Replace with Twenty application ID
};

View file

@ -56,7 +56,6 @@ export type RoleManifest = SyncableEntityOptions & {
canBeAssignedToUsers?: boolean;
canBeAssignedToAgents?: boolean;
canBeAssignedToApiKeys?: boolean;
canBeAssignedToApplications?: boolean;
objectPermissions?: ObjectPermission[];
fieldPermissions?: FieldPermission[];
permissionFlags?: PermissionFlagType[];