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:
@@ -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)
|
- A Twenty workspace and an API key (create one at https://app.twenty.com/settings/api-webhooks)
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx create-twenty-app@latest my-twenty-app
|
npx create-twenty-app@latest my-twenty-app
|
||||||
cd my-twenty-app
|
cd my-twenty-app
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "create-twenty-app",
|
"name": "create-twenty-app",
|
||||||
"version": "0.2.3",
|
"version": "0.2.4",
|
||||||
"description": "Command-line interface to create Twenty application",
|
"description": "Command-line interface to create Twenty application",
|
||||||
"main": "dist/cli.cjs",
|
"main": "dist/cli.cjs",
|
||||||
"bin": "dist/cli.cjs",
|
"bin": "dist/cli.cjs",
|
||||||
|
|||||||
@@ -1,137 +1,29 @@
|
|||||||
import js from '@eslint/js';
|
import js from '@eslint/js';
|
||||||
import typescriptEslint from '@typescript-eslint/eslint-plugin';
|
import tseslint from 'typescript-eslint';
|
||||||
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';
|
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
// Base JS rules
|
// Base JS recommended rules
|
||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
|
|
||||||
// Global ignores
|
// TypeScript recommended rules
|
||||||
{
|
...tseslint.configs.recommended,
|
||||||
ignores: ['**/node_modules/**', '**/dist/**', '**/coverage/**'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// Base config for TS/JS files
|
|
||||||
{
|
{
|
||||||
files: ['**/*.{js,jsx,ts,tsx}'],
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
languageOptions: {
|
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: {
|
parserOptions: {
|
||||||
ecmaFeatures: {
|
project: true,
|
||||||
jsx: true,
|
tsconfigRootDir: import.meta.dirname,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: {
|
|
||||||
'@typescript-eslint': typescriptEslint,
|
|
||||||
},
|
|
||||||
rules: {
|
rules: {
|
||||||
// Turn off base rule and use TS-aware versions
|
// Common TypeScript-friendly tweaks
|
||||||
'no-redeclare': 'off',
|
'@typescript-eslint/no-unused-vars': [
|
||||||
'@typescript-eslint/no-redeclare': 'error',
|
'warn',
|
||||||
|
{ argsIgnorePattern: '^_' },
|
||||||
'@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,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
'@typescript-eslint/no-empty-function': 'off',
|
'no-unused-vars': 'off', // handled by TS rule
|
||||||
'@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',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ const createDefaultServerlessFunctionRoleConfig = async ({
|
|||||||
appDirectory: string;
|
appDirectory: string;
|
||||||
defaultServerlessFunctionRoleUniversalIdentifier: string;
|
defaultServerlessFunctionRoleUniversalIdentifier: string;
|
||||||
}) => {
|
}) => {
|
||||||
const content = `import { PermissionFlag, type RoleConfig } from 'twenty-sdk';
|
const content = `import { type RoleConfig } from 'twenty-sdk';
|
||||||
|
|
||||||
export const functionRole: RoleConfig = {
|
export const functionRole: RoleConfig = {
|
||||||
universalIdentifier: '${defaultServerlessFunctionRoleUniversalIdentifier}',
|
universalIdentifier: '${defaultServerlessFunctionRoleUniversalIdentifier}',
|
||||||
@@ -168,13 +168,17 @@ const createPackageJson = async ({
|
|||||||
uninstall: 'twenty app uninstall',
|
uninstall: 'twenty app uninstall',
|
||||||
help: 'twenty help',
|
help: 'twenty help',
|
||||||
auth: 'twenty auth login',
|
auth: 'twenty auth login',
|
||||||
|
lint: 'eslint',
|
||||||
|
'lint-fix': 'eslint --fix',
|
||||||
},
|
},
|
||||||
dependencies: {
|
dependencies: {
|
||||||
'twenty-sdk': '0.2.3',
|
'twenty-sdk': '0.2.4',
|
||||||
},
|
},
|
||||||
devDependencies: {
|
devDependencies: {
|
||||||
'@types/node': '^24.7.2',
|
|
||||||
typescript: '^5.9.3',
|
typescript: '^5.9.3',
|
||||||
|
'@types/node': '^24.7.2',
|
||||||
|
eslint: '^9.32.0',
|
||||||
|
'typescript-eslint': '^8.50.0',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hello-world",
|
"name": "hello-world",
|
||||||
"version": "0.2.1",
|
"version": "0.2.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^24.5.0",
|
"node": "^24.5.0",
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
"auth": "twenty auth login"
|
"auth": "twenty auth login"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"twenty-sdk": "0.2.2"
|
"twenty-sdk": "0.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^24.7.2"
|
"@types/node": "^24.7.2"
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ export const functionRole: RoleConfig = {
|
|||||||
canBeAssignedToAgents: false,
|
canBeAssignedToAgents: false,
|
||||||
canBeAssignedToUsers: false,
|
canBeAssignedToUsers: false,
|
||||||
canBeAssignedToApiKeys: false,
|
canBeAssignedToApiKeys: false,
|
||||||
canBeAssignedToApplications: true,
|
|
||||||
objectPermissions: [
|
objectPermissions: [
|
||||||
{
|
{
|
||||||
objectNameSingular: 'postCard',
|
objectNameSingular: 'postCard',
|
||||||
|
|||||||
@@ -1,89 +1,8 @@
|
|||||||
# Deprecated: twenty-cli
|
# 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
|
```bash
|
||||||
npm uninstall twenty-cli
|
npm uninstall twenty-cli
|
||||||
npm install -g twenty-sdk
|
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)
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "twenty-cli",
|
"name": "twenty-cli",
|
||||||
"version": "0.3.0",
|
"version": "0.3.1",
|
||||||
"description": "[DEPRECATED] Use twenty-sdk instead: https://www.npmjs.com/package/twenty-sdk",
|
"description": "[DEPRECATED] Use twenty-sdk instead: https://www.npmjs.com/package/twenty-sdk",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "echo 'use npx nx build'",
|
"build": "echo 'use npx nx build'",
|
||||||
|
|||||||
@@ -19,5 +19,498 @@ Apps let you build and manage Twenty customizations **as code**. Instead of conf
|
|||||||
**Coming soon:**
|
**Coming soon:**
|
||||||
- Custom UI layouts and components
|
- 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 one‑time 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 app’s 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**: key–value 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 workspace’s objects and actions. The field `functionRoleUniversalIdentifier` in `application.config.ts` designates the default role used by your app’s 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 least‑privilege: 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 least‑privilege.
|
||||||
|
- 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`: Short‑lived key scoped to your application’s 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 key’s 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 least‑privilege. Grant only the permissions your functions need, then point `functionRoleUniversalIdentifier` to that role’s 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
|
||||||
|
|||||||
@@ -915,7 +915,6 @@ export type CreateRoleInput = {
|
|||||||
canAccessAllTools?: InputMaybe<Scalars['Boolean']>;
|
canAccessAllTools?: InputMaybe<Scalars['Boolean']>;
|
||||||
canBeAssignedToAgents?: InputMaybe<Scalars['Boolean']>;
|
canBeAssignedToAgents?: InputMaybe<Scalars['Boolean']>;
|
||||||
canBeAssignedToApiKeys?: InputMaybe<Scalars['Boolean']>;
|
canBeAssignedToApiKeys?: InputMaybe<Scalars['Boolean']>;
|
||||||
canBeAssignedToApplications?: InputMaybe<Scalars['Boolean']>;
|
|
||||||
canBeAssignedToUsers?: InputMaybe<Scalars['Boolean']>;
|
canBeAssignedToUsers?: InputMaybe<Scalars['Boolean']>;
|
||||||
canDestroyAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
|
canDestroyAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
|
||||||
canReadAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
|
canReadAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
|
||||||
@@ -4424,7 +4423,6 @@ export type UpdateRolePayload = {
|
|||||||
canAccessAllTools?: InputMaybe<Scalars['Boolean']>;
|
canAccessAllTools?: InputMaybe<Scalars['Boolean']>;
|
||||||
canBeAssignedToAgents?: InputMaybe<Scalars['Boolean']>;
|
canBeAssignedToAgents?: InputMaybe<Scalars['Boolean']>;
|
||||||
canBeAssignedToApiKeys?: InputMaybe<Scalars['Boolean']>;
|
canBeAssignedToApiKeys?: InputMaybe<Scalars['Boolean']>;
|
||||||
canBeAssignedToApplications?: InputMaybe<Scalars['Boolean']>;
|
|
||||||
canBeAssignedToUsers?: InputMaybe<Scalars['Boolean']>;
|
canBeAssignedToUsers?: InputMaybe<Scalars['Boolean']>;
|
||||||
canDestroyAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
|
canDestroyAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
|
||||||
canReadAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
|
canReadAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
|
||||||
|
|||||||
@@ -898,7 +898,6 @@ export type CreateRoleInput = {
|
|||||||
canAccessAllTools?: InputMaybe<Scalars['Boolean']>;
|
canAccessAllTools?: InputMaybe<Scalars['Boolean']>;
|
||||||
canBeAssignedToAgents?: InputMaybe<Scalars['Boolean']>;
|
canBeAssignedToAgents?: InputMaybe<Scalars['Boolean']>;
|
||||||
canBeAssignedToApiKeys?: InputMaybe<Scalars['Boolean']>;
|
canBeAssignedToApiKeys?: InputMaybe<Scalars['Boolean']>;
|
||||||
canBeAssignedToApplications?: InputMaybe<Scalars['Boolean']>;
|
|
||||||
canBeAssignedToUsers?: InputMaybe<Scalars['Boolean']>;
|
canBeAssignedToUsers?: InputMaybe<Scalars['Boolean']>;
|
||||||
canDestroyAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
|
canDestroyAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
|
||||||
canReadAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
|
canReadAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
|
||||||
@@ -4253,7 +4252,6 @@ export type UpdateRolePayload = {
|
|||||||
canAccessAllTools?: InputMaybe<Scalars['Boolean']>;
|
canAccessAllTools?: InputMaybe<Scalars['Boolean']>;
|
||||||
canBeAssignedToAgents?: InputMaybe<Scalars['Boolean']>;
|
canBeAssignedToAgents?: InputMaybe<Scalars['Boolean']>;
|
||||||
canBeAssignedToApiKeys?: InputMaybe<Scalars['Boolean']>;
|
canBeAssignedToApiKeys?: InputMaybe<Scalars['Boolean']>;
|
||||||
canBeAssignedToApplications?: InputMaybe<Scalars['Boolean']>;
|
|
||||||
canBeAssignedToUsers?: InputMaybe<Scalars['Boolean']>;
|
canBeAssignedToUsers?: InputMaybe<Scalars['Boolean']>;
|
||||||
canDestroyAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
|
canDestroyAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
|
||||||
canReadAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
|
canReadAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
|
||||||
|
|||||||
+129
-43
@@ -30,66 +30,152 @@ npm install twenty-sdk
|
|||||||
yarn add twenty-sdk
|
yarn add twenty-sdk
|
||||||
```
|
```
|
||||||
|
|
||||||
## Getting started
|
## Usage
|
||||||
You can either scaffold a new app or add the SDK to an existing one.
|
|
||||||
|
|
||||||
- Start new (recommended):
|
```
|
||||||
```bash
|
Usage: twenty [options] [command]
|
||||||
npx create-twenty-app@latest my-twenty-app
|
|
||||||
cd my-twenty-app
|
CLI for Twenty application development
|
||||||
```
|
|
||||||
- Existing project: install the SDK as shown above, then use the CLI below.
|
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
|
```bash
|
||||||
# Authenticate using your API key (CLI will prompt for it)
|
# Login interactively (recommended)
|
||||||
twenty auth login
|
twenty auth login
|
||||||
|
|
||||||
# Add a new entity to your application (guided prompts)
|
# Provide values in flags
|
||||||
twenty app add
|
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
|
# Login interactively for a specific workspace profile
|
||||||
twenty app generate
|
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
|
twenty app dev
|
||||||
|
|
||||||
# One‑time 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
|
twenty app sync
|
||||||
|
|
||||||
# Uninstall the application from the current workspace
|
# Add a new object interactively
|
||||||
twenty app uninstall
|
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)
|
## Configuration
|
||||||
```typescript
|
|
||||||
// Example: import what you need from the SDK
|
The CLI stores configuration per user in a JSON file:
|
||||||
import { /* your exports */ } from 'twenty-sdk';
|
|
||||||
|
- 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
|
Notes:
|
||||||
Applications are currently stored in [`twenty/packages/twenty-apps`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps).
|
|
||||||
|
|
||||||
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
|
## Troubleshooting
|
||||||
- Auth errors: run `twenty auth login` again and ensure the API key has the required permissions.
|
- Auth errors: run `twenty auth login` again and ensure the API key has the required permissions.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "twenty-sdk",
|
"name": "twenty-sdk",
|
||||||
"version": "0.2.3",
|
"version": "0.2.4",
|
||||||
"main": "dist/index.cjs",
|
"main": "dist/index.cjs",
|
||||||
"module": "dist/index.mjs",
|
"module": "dist/index.mjs",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export class AppCommand {
|
|||||||
});
|
});
|
||||||
|
|
||||||
appCommand
|
appCommand
|
||||||
.command('generate [outputPath]')
|
.command('generate [appPath]')
|
||||||
.description('Generate Twenty client')
|
.description('Generate Twenty client')
|
||||||
.action(async (appPath?: string) => {
|
.action(async (appPath?: string) => {
|
||||||
await this.generateCommand.execute(formatPath(appPath));
|
await this.generateCommand.execute(formatPath(appPath));
|
||||||
|
|||||||
+19
@@ -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`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
-1
@@ -13,5 +13,4 @@ export const FLAT_ROLE_EDITABLE_PROPERTIES: (keyof FlatRole)[] = [
|
|||||||
'canBeAssignedToUsers',
|
'canBeAssignedToUsers',
|
||||||
'canBeAssignedToAgents',
|
'canBeAssignedToAgents',
|
||||||
'canBeAssignedToApiKeys',
|
'canBeAssignedToApiKeys',
|
||||||
'canBeAssignedToApplications',
|
|
||||||
];
|
];
|
||||||
|
|||||||
-2
@@ -44,8 +44,6 @@ export const fromCreateRoleInputToFlatRoleToCreate = ({
|
|||||||
canBeAssignedToUsers: createRoleInput.canBeAssignedToUsers ?? true,
|
canBeAssignedToUsers: createRoleInput.canBeAssignedToUsers ?? true,
|
||||||
canBeAssignedToAgents: createRoleInput.canBeAssignedToAgents ?? true,
|
canBeAssignedToAgents: createRoleInput.canBeAssignedToAgents ?? true,
|
||||||
canBeAssignedToApiKeys: createRoleInput.canBeAssignedToApiKeys ?? true,
|
canBeAssignedToApiKeys: createRoleInput.canBeAssignedToApiKeys ?? true,
|
||||||
canBeAssignedToApplications:
|
|
||||||
createRoleInput.canBeAssignedToApplications ?? true,
|
|
||||||
isEditable: true,
|
isEditable: true,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
|
|||||||
-1
@@ -18,7 +18,6 @@ export const fromRoleEntityToFlatRole = (role: RoleEntity): FlatRole => {
|
|||||||
canBeAssignedToUsers: role.canBeAssignedToUsers,
|
canBeAssignedToUsers: role.canBeAssignedToUsers,
|
||||||
canBeAssignedToAgents: role.canBeAssignedToAgents,
|
canBeAssignedToAgents: role.canBeAssignedToAgents,
|
||||||
canBeAssignedToApiKeys: role.canBeAssignedToApiKeys,
|
canBeAssignedToApiKeys: role.canBeAssignedToApiKeys,
|
||||||
canBeAssignedToApplications: role.canBeAssignedToApplications,
|
|
||||||
workspaceId: role.workspaceId,
|
workspaceId: role.workspaceId,
|
||||||
createdAt: role.createdAt.toISOString(),
|
createdAt: role.createdAt.toISOString(),
|
||||||
updatedAt: role.updatedAt.toISOString(),
|
updatedAt: role.updatedAt.toISOString(),
|
||||||
|
|||||||
@@ -70,9 +70,4 @@ export class CreateRoleInput {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Field({ nullable: true })
|
@Field({ nullable: true })
|
||||||
canBeAssignedToApiKeys?: boolean;
|
canBeAssignedToApiKeys?: boolean;
|
||||||
|
|
||||||
@IsBoolean()
|
|
||||||
@IsOptional()
|
|
||||||
@Field({ nullable: true })
|
|
||||||
canBeAssignedToApplications?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,11 +71,6 @@ export class UpdateRolePayload {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Field({ nullable: true })
|
@Field({ nullable: true })
|
||||||
canBeAssignedToApiKeys?: boolean;
|
canBeAssignedToApiKeys?: boolean;
|
||||||
|
|
||||||
@IsBoolean()
|
|
||||||
@IsOptional()
|
|
||||||
@Field({ nullable: true })
|
|
||||||
canBeAssignedToApplications?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
|
|||||||
@@ -73,9 +73,6 @@ export class RoleEntity extends SyncableEntity implements Required<RoleEntity> {
|
|||||||
@Column({ nullable: false, default: true })
|
@Column({ nullable: false, default: true })
|
||||||
canBeAssignedToApiKeys: boolean;
|
canBeAssignedToApiKeys: boolean;
|
||||||
|
|
||||||
@Column({ nullable: false, default: true })
|
|
||||||
canBeAssignedToApplications: boolean;
|
|
||||||
|
|
||||||
@OneToMany(
|
@OneToMany(
|
||||||
() => RoleTargetEntity,
|
() => RoleTargetEntity,
|
||||||
(roleTargets: RoleTargetEntity) => roleTargets.role,
|
(roleTargets: RoleTargetEntity) => roleTargets.role,
|
||||||
|
|||||||
-1
@@ -24,7 +24,6 @@ export const STANDARD_FLAT_ROLE_METADATA_BUILDERS_BY_ROLE_NAME = {
|
|||||||
canBeAssignedToUsers: true,
|
canBeAssignedToUsers: true,
|
||||||
canBeAssignedToAgents: false,
|
canBeAssignedToAgents: false,
|
||||||
canBeAssignedToApiKeys: true,
|
canBeAssignedToApiKeys: true,
|
||||||
canBeAssignedToApplications: false,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
} satisfies {
|
} satisfies {
|
||||||
|
|||||||
-3
@@ -20,7 +20,6 @@ export type CreateStandardRoleContext = {
|
|||||||
canBeAssignedToUsers: boolean;
|
canBeAssignedToUsers: boolean;
|
||||||
canBeAssignedToAgents: boolean;
|
canBeAssignedToAgents: boolean;
|
||||||
canBeAssignedToApiKeys: boolean;
|
canBeAssignedToApiKeys: boolean;
|
||||||
canBeAssignedToApplications: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateStandardRoleArgs = StandardBuilderArgs<'role'> & {
|
export type CreateStandardRoleArgs = StandardBuilderArgs<'role'> & {
|
||||||
@@ -43,7 +42,6 @@ export const createStandardRoleFlatMetadata = ({
|
|||||||
canBeAssignedToUsers,
|
canBeAssignedToUsers,
|
||||||
canBeAssignedToAgents,
|
canBeAssignedToAgents,
|
||||||
canBeAssignedToApiKeys,
|
canBeAssignedToApiKeys,
|
||||||
canBeAssignedToApplications,
|
|
||||||
},
|
},
|
||||||
workspaceId,
|
workspaceId,
|
||||||
twentyStandardApplicationId,
|
twentyStandardApplicationId,
|
||||||
@@ -68,7 +66,6 @@ export const createStandardRoleFlatMetadata = ({
|
|||||||
canBeAssignedToUsers,
|
canBeAssignedToUsers,
|
||||||
canBeAssignedToAgents,
|
canBeAssignedToAgents,
|
||||||
canBeAssignedToApiKeys,
|
canBeAssignedToApiKeys,
|
||||||
canBeAssignedToApplications,
|
|
||||||
workspaceId,
|
workspaceId,
|
||||||
applicationId: twentyStandardApplicationId,
|
applicationId: twentyStandardApplicationId,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
|
|||||||
-1
@@ -15,6 +15,5 @@ export const ADMIN_ROLE: StandardRoleDefinition = {
|
|||||||
canBeAssignedToUsers: true,
|
canBeAssignedToUsers: true,
|
||||||
canBeAssignedToAgents: false,
|
canBeAssignedToAgents: false,
|
||||||
canBeAssignedToApiKeys: true,
|
canBeAssignedToApiKeys: true,
|
||||||
canBeAssignedToApplications: false,
|
|
||||||
applicationId: null, // TODO: Replace with Twenty application ID
|
applicationId: null, // TODO: Replace with Twenty application ID
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ export type RoleManifest = SyncableEntityOptions & {
|
|||||||
canBeAssignedToUsers?: boolean;
|
canBeAssignedToUsers?: boolean;
|
||||||
canBeAssignedToAgents?: boolean;
|
canBeAssignedToAgents?: boolean;
|
||||||
canBeAssignedToApiKeys?: boolean;
|
canBeAssignedToApiKeys?: boolean;
|
||||||
canBeAssignedToApplications?: boolean;
|
|
||||||
objectPermissions?: ObjectPermission[];
|
objectPermissions?: ObjectPermission[];
|
||||||
fieldPermissions?: FieldPermission[];
|
fieldPermissions?: FieldPermission[];
|
||||||
permissionFlags?: PermissionFlagType[];
|
permissionFlags?: PermissionFlagType[];
|
||||||
|
|||||||
Reference in New Issue
Block a user