2081 extensibility publish cli tools and update doc with recent changes (#17495)

- increase to 0.4.0
- update READMEs and doc
This commit is contained in:
martmull
2026-01-27 21:49:33 +01:00
committed by GitHub
parent 8fe02c5c19
commit b9586769b9
16 changed files with 117 additions and 168 deletions
+12 -9
View File
@@ -15,9 +15,12 @@
Create Twenty App is the official scaffolding CLI for building apps on top of [Twenty CRM](https://twenty.com). It sets up a readytorun project that works seamlessly with the [twenty-sdk](https://www.npmjs.com/package/twenty-sdk).
- Zeroconfig project bootstrap
- Preconfigured scripts for auth, generate, dev sync, oneoff sync, uninstall
- Preconfigured scripts for auth, dev mode (watch & sync), generate, uninstall, and function management
- Strong TypeScript support and typed client generation
## Documentation
See Twenty application documentation https://docs.twenty.com/developers/extend/capabilities/apps
## Prerequisites
- Node.js 24+ (recommended) and Yarn 4
- A Twenty workspace and an API key (create one at https://app.twenty.com/settings/api-webhooks)
@@ -32,7 +35,7 @@ cd my-twenty-app
corepack enable
yarn install
# Get Help
# Get help
yarn run help
# Authenticate using your API key (you'll be prompted)
@@ -44,15 +47,15 @@ yarn entity:add
# Generate a typed Twenty client and workspace entity types
yarn app:generate
# Start dev mode: automatically syncs local changes to your workspace
# Start dev mode: watches, builds, and syncs local changes to your workspace
yarn app:dev
# Or run a onetime sync
yarn app:sync
# Watch your application's functions logs
# Watch your application's function logs
yarn function:logs
# Execute a function with a JSON payload
yarn function:execute -n my-function -p '{"key": "value"}'
# Uninstall the application from the current workspace
yarn app:uninstall
```
@@ -64,9 +67,9 @@ yarn app:uninstall
- Example placeholders to help you add entities, actions, and sync logic
## Next steps
- Explore the generated project and add your first entity with `yarn entity:add`.
- Explore the generated project and add your first entity with `yarn entity:add` (functions, front components, objects, roles).
- Keep your types uptodate using `yarn app:generate`.
- Use `yarn app:dev` while you iterate to see changes instantly in your workspace.
- Use `yarn app:dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
## Publish your application
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "create-twenty-app",
"version": "0.3.1",
"version": "0.4.0",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",
@@ -14,12 +14,6 @@ Then, start development mode to sync your app and watch for changes:
yarn app:dev
```
Or run a one-time sync:
```bash
yarn app:sync
```
Open your Twenty instance and go to `/settings/applications` section to see the result.
## Available Commands
@@ -29,11 +23,12 @@ Open your Twenty instance and go to `/settings/applications` section to see the
yarn auth:login # Authenticate with Twenty
yarn auth:logout # Remove credentials
yarn auth:status # Check auth status
yarn auth:switch # Switch default workspace
yarn auth:list # List all configured workspaces
# Application
yarn app:dev # Start dev mode (sync + watch)
yarn app:sync # One-time sync
yarn entity:add # Add a new entity (function, object, role)
yarn app:dev # Start dev mode (watch, build, and sync)
yarn entity:add # Add a new entity (function, front-component, object, role)
yarn app:generate # Generate typed Twenty client
yarn function:logs # Stream function logs
yarn function:execute # Execute a function with JSON payload
@@ -67,8 +67,7 @@ describe('copyBaseApplicationProject', () => {
const packageJson = await fs.readJson(packageJsonPath);
expect(packageJson.name).toBe('my-test-app');
expect(packageJson.version).toBe('0.1.0');
expect(packageJson.dependencies['twenty-sdk']).toBe('0.3.1');
expect(packageJson.scripts['app:sync']).toBe('twenty app:sync');
expect(packageJson.dependencies['twenty-sdk']).toBe('0.4.0');
expect(packageJson.scripts['app:dev']).toBe('twenty app:dev');
});
@@ -238,8 +238,6 @@ const createPackageJson = async ({
'auth:switch': 'twenty auth:switch',
'auth:list': 'twenty auth:list',
'app:dev': 'twenty app:dev',
'app:build': 'twenty app:build',
'app:sync': 'twenty app:sync',
'entity:add': 'twenty entity:add',
'app:generate': 'twenty app:generate',
'function:logs': 'twenty function:logs',
@@ -250,7 +248,7 @@ const createPackageJson = async ({
'lint:fix': 'eslint --fix',
},
dependencies: {
'twenty-sdk': '0.3.1',
'twenty-sdk': '0.4.0',
},
devDependencies: {
typescript: '^5.9.3',
@@ -48,15 +48,12 @@ From here you can:
```bash filename="Terminal"
# Add a new entity to your application (guided)
yarn app:create-entity
yarn entity:add
# Generate a typed Twenty client and workspace entity types
yarn app:generate
# Run a onetime sync (instead of watch mode)
yarn app:sync
# Watch your application's functions logs
# Watch your application's function logs
yarn function:logs
# Execute a function by name
@@ -66,7 +63,7 @@ yarn function:execute -n my-function -p '{"name": "test"}'
yarn app:uninstall
# Display commands' help
yarn app: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).
@@ -95,11 +92,11 @@ my-twenty-app/
tsconfig.json
README.md
src/
app/
application.config.ts # Required - main application configuration
default-function.role.ts # Default role for serverless functions
// your entities (*.object.ts, *.function.ts, *.role.ts)
utils/ # Optional - handler implementations & utilities
application.config.ts # Required - main application configuration
default-function.role.ts # Default role for serverless functions
hello-world.function.ts # Example serverless function
hello-world.front-component.tsx # Example front component
// your entities (*.object.ts, *.function.ts, *.front-component.tsx, *.role.ts)
```
### Convention-over-configuration
@@ -110,6 +107,7 @@ Applications use a **convention-over-configuration** approach where entities are
|-------------|-------------|
| `*.object.ts` | Custom object definitions |
| `*.function.ts` | Serverless function definitions |
| `*.front-component.tsx` | Front component definitions |
| `*.role.ts` | Role definitions |
### Supported folder organizations
@@ -118,55 +116,58 @@ You can organize your entities in any of these patterns:
**Traditional (by type):**
```text
src/app/
src/
├── application.config.ts
├── objects/
│ └── postCard.object.ts
├── functions/
│ └── createPostCard.function.ts
├── components/
│ └── card.front-component.tsx
└── roles/
└── admin.role.ts
```
**Feature-based:**
```text
src/app/
src/
├── application.config.ts
└── post-card/
├── postCard.object.ts
├── createPostCard.function.ts
├── card.front-component.tsx
└── postCardAdmin.role.ts
```
**Flat:**
```text
src/app/
src/
├── application.config.ts
├── postCard.object.ts
├── createPostCard.function.ts
├── card.front-component.tsx
└── admin.role.ts
```
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.
- **package.json**: Declares the app name, version, engines (Node 24+, Yarn 4), and adds `twenty-sdk` plus scripts like `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall`, and `auth:login` 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/app/**: The main place where you define your application-as-code:
- **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.ts`: Role definitions used by your serverless functions. See "Default function role" below.
- `*.object.ts`: Custom object definitions.
- `*.function.ts`: Serverless function definitions.
- **src/utils/**: Optional folder for handler implementations and utilities.
- `*.front-component.tsx`: Front component definitions.
Later commands will add more files and folders:
- `yarn app:generate` will create a `generated/` folder (typed Twenty client + workspace types).
- `yarn app:create-entity` will add entity definition files under `src/app/` for your custom objects, functions, or roles.
l
- `yarn entity:add` will add entity definition files under `src/` for your custom objects, functions, front components, or roles.
## Authentication
@@ -297,63 +298,12 @@ Key points:
- The `universalIdentifier` must be unique and stable across deployments.
- Each field requires a `name`, `type`, `label`, and its own stable `universalIdentifier`.
- The `fields` array is optional — you can define objects without custom fields.
- You can scaffold new objects using `yarn app:create-entity`, which guides you through naming, fields, and relationships.
- You can scaffold new objects using `yarn entity:add`, which guides you through naming, fields, and relationships.
<Note>
**Base fields are created automatically.** When you define a custom object, Twenty automatically adds standard fields such as `name`, `createdAt`, `updatedAt`, `createdBy`, `position`, and `deletedAt`. You don't need to define these in your `fields` array — only add your custom fields.
</Note>
<Accordion title="Alternative: Decorator-based syntax">
You can also define objects using TypeScript decorators. This approach uses class-based syntax with `@Object`, `@Field`, and `@Relation` decorators:
```typescript
import {
type AddressField,
Field,
FieldType,
type FullNameField,
Object,
OnDeleteAction,
Relation,
RelationType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
import { type Note } from '../../generated';
@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;
@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[];
}
```
Note: The decorator approach requires `experimentalDecorators` in your TypeScript config.
</Accordion>
### Application config (application.config.ts)
@@ -463,14 +413,9 @@ Each function file uses `defineFunction()` to export a configuration with a hand
// src/app/createPostCard.function.ts
import { defineFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '../../generated';
import Twenty, { type Person } from '~/generated';
const handler = async (
params:
| RoutePayload
| DatabaseEventPayload<ObjectRecordCreateEvent<Person>>
| CronPayload,
) => {
const handler = async (params: RoutePayload) => {
const client = new Twenty(); // generated typed client
const name = 'name' in params.queryStringParameters
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
@@ -501,18 +446,18 @@ export default defineFunction({
isAuthRequired: false,
},
// Cron trigger (CRON pattern)
{
universalIdentifier: 'dd802808-0695-49e1-98c9-d5c9e2704ce2',
type: 'cron',
pattern: '0 0 1 1 *',
},
// {
// universalIdentifier: 'dd802808-0695-49e1-98c9-d5c9e2704ce2',
// type: 'cron',
// pattern: '0 0 1 1 *',
// },
// Database event trigger
{
universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156',
type: 'databaseEvent',
eventName: 'person.updated',
updatedFields: ['name'],
},
// {
// universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156',
// type: 'databaseEvent',
// eventName: 'person.updated',
// updatedFields: ['name'],
// },
],
});
```
@@ -620,7 +565,7 @@ const handler = async (event: RoutePayload) => {
You can create new functions in two ways:
- **Scaffolded**: Run `yarn app:create-entity` and choose the option to add a new function. This generates a starter file with a handler and config.
- **Scaffolded**: Run `yarn entity:add` and choose the option to add a new function. This generates a starter file with a handler and config.
- **Manual**: Create a new `*.function.ts` file and use `defineFunction()`, following the same pattern.
### Generated typed client
@@ -628,13 +573,13 @@ You can create new functions in two ways:
Run yarn app:generate to create a local typed client in generated/ based on your workspace schema. Use it in your functions:
```typescript
import Twenty from './generated';
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 app:generate`. Re-run after changing your objects and `yarn app:sync` or when onboarding to a new workspace.
The client is re-generated by `yarn app:generate`. Re-run after changing your objects or when onboarding to a new workspace.
#### Runtime credentials in serverless functions
@@ -666,25 +611,29 @@ 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"
"auth:login": "twenty auth:login",
"auth:logout": "twenty auth:logout",
"auth:status": "twenty auth:status",
"auth:switch": "twenty auth:switch",
"auth:list": "twenty auth:list",
"app:dev": "twenty app:dev",
"app:generate": "twenty app:generate",
"app:uninstall": "twenty app:uninstall",
"entity:add": "twenty entity:add",
"function:logs": "twenty function:logs",
"function:execute": "twenty function:execute",
"help": "twenty help"
}
}
```
Now you can run the same commands via Yarn, e.g. `yarn app:dev`, `yarn app:sync`, etc.
Now you can run the same commands via Yarn, e.g. `yarn app:dev`, `yarn app:generate`, etc.
## Troubleshooting
- Authentication errors: run `yarn auth:login` 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 app:generate` and then `yarn app:dev`.
- Types or client missing/outdated: run `yarn app:generate`.
- Dev mode not syncing: ensure `yarn app:dev` is running and that changes are not ignored by your environment.
Discord Help Channel: https://discord.com/channels/1130383047699738754/1130386664812982322
+39 -24
View File
@@ -15,9 +15,12 @@
A CLI and SDK to develop, build, and publish applications that extend [Twenty CRM](https://twenty.com).
- Typesafe client and workspace entity typings
- Builtin CLI for auth, generate, dev sync, oneoff sync, and uninstall
- Builtin CLI for auth, dev mode (watch & sync), generate, uninstall, and function management
- Works great with the scaffolder: [create-twenty-app](https://www.npmjs.com/package/create-twenty-app)
## Documentation
See Twenty application documentation https://docs.twenty.com/developers/extend/capabilities/apps
## Prerequisites
- Node.js 24+ (recommended) and Yarn 4
- A Twenty workspace and an API key. Generate one at https://app.twenty.com/settings/api-webhooks
@@ -43,8 +46,17 @@ Options:
-h, --help display help for command
Commands:
auth Authentication commands
app Application development commands
auth:login Authenticate with Twenty
auth:logout Remove authentication credentials
auth:status Check authentication status
auth:switch Switch the default workspace
auth:list List all configured workspaces
app:dev Watch and sync local application changes
app:generate Generate Twenty client
app:uninstall Uninstall application from Twenty
entity:add Add a new entity to your application
function:logs Watch application function logs
function:execute Execute a serverless function with a JSON payload
help [command] display help for command
```
@@ -108,27 +120,27 @@ twenty auth:switch production
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]` — Start development mode: 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:dev [appPath]`Start development mode: watch and sync local application changes.
- Behavior: Builds your application (functions and front components), computes the manifest, syncs everything to your workspace, then watches the directory for changes and re-syncs automatically. Displays an interactive UI showing build and sync status in real time. 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:generate [appPath]` — Generate the typed Twenty client for your application.
### Entity
- `twenty entity:add [entityType]` — Add a new entity to your application.
- Arguments:
- `entityType`: one of `function` or `object`. If omitted, an interactive prompt is shown.
- `entityType`: one of `function`, `front-component`, `object`, or `role`. 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.
- `object`: prompts for singular/plural names and labels, then creates a `*.object.ts` definition file.
- `function`: prompts for a name and scaffolds a `*.function.ts` serverless function file.
- `front-component`: prompts for a name and scaffolds a `*.front-component.tsx` file.
- `role`: prompts for a name and scaffolds a `*.role.ts` role definition file.
- `twenty app:generate [appPath]` — Generate the typed Twenty client for your application.
### Function
- `twenty function:logs [appPath]` — Stream application function logs.
- Options:
@@ -144,24 +156,27 @@ Application development commands.
Examples:
```bash
# Start dev mode with default debounce
# Start dev mode (watch, build, and sync)
twenty app:dev
# Start dev mode with custom workspace profile
# Start dev mode with a 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
# Add a new object interactively
# Add a new entity interactively
twenty entity:add
# Add a new function
twenty entity:add function
# Add a new front component
twenty entity:add front-component
# Generate client types
twenty app:generate
# Uninstall the app from the workspace
twenty app:uninstall
# Watch all function logs
twenty function:logs
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "twenty-sdk",
"version": "0.3.1",
"version": "0.4.0",
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
@@ -15,8 +15,6 @@
"auth:switch": "twenty auth:switch",
"auth:list": "twenty auth:list",
"app:dev": "twenty app:dev",
"app:build": "twenty app:build",
"app:sync": "twenty app:sync",
"entity:add": "twenty entity:add",
"app:generate": "twenty app:generate",
"function:logs": "twenty function:logs",
@@ -219,8 +219,6 @@ export const EXPECTED_MANIFEST: ApplicationManifest = {
'auth:switch': 'twenty auth:switch',
'auth:list': 'twenty auth:list',
'app:dev': 'twenty app:dev',
'app:build': 'twenty app:build',
'app:sync': 'twenty app:sync',
'entity:add': 'twenty entity:add',
'app:generate': 'twenty app:generate',
'function:logs': 'twenty function:logs',
@@ -15,8 +15,6 @@
"auth:switch": "twenty auth:switch",
"auth:list": "twenty auth:list",
"app:dev": "twenty app:dev",
"app:build": "twenty app:build",
"app:sync": "twenty app:sync",
"entity:add": "twenty entity:add",
"app:generate": "twenty app:generate",
"function:logs": "twenty function:logs",
@@ -21,8 +21,6 @@ export const EXPECTED_MANIFEST: ApplicationManifest = {
'auth:switch': 'twenty auth:switch',
'auth:list': 'twenty auth:list',
'app:dev': 'twenty app:dev',
'app:build': 'twenty app:build',
'app:sync': 'twenty app:sync',
'entity:add': 'twenty entity:add',
'app:generate': 'twenty app:generate',
'function:logs': 'twenty function:logs',
@@ -15,8 +15,6 @@
"auth:switch": "twenty auth:switch",
"auth:list": "twenty auth:list",
"app:dev": "twenty app:dev",
"app:build": "twenty app:build",
"app:sync": "twenty app:sync",
"entity:add": "twenty entity:add",
"app:generate": "twenty app:generate",
"function:logs": "twenty function:logs",
@@ -2,7 +2,7 @@ import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-exec
import { getFrontComponentBaseFile } from '@/cli/utilities/entity/entity-front-component-template';
import { getFunctionBaseFile } from '@/cli/utilities/entity/entity-function-template';
import { convertToLabel } from '@/cli/utilities/entity/entity-label';
import { getNewObjectFileContent } from '@/cli/utilities/entity/entity-object-template';
import { getObjectBaseFile } from '@/cli/utilities/entity/entity-object-template';
import { getRoleBaseFile } from '@/cli/utilities/entity/entity-role-template';
import chalk from 'chalk';
import * as fs from 'fs-extra';
@@ -45,7 +45,7 @@ export class EntityAddCommand {
// Use *.object.ts naming convention
const objectFileName = `${camelcase(name)}.object.ts`;
const decoratedObject = getNewObjectFileContent({
const decoratedObject = getObjectBaseFile({
data: entityData,
name,
});
@@ -1,8 +1,8 @@
import { getNewObjectFileContent } from '@/cli/utilities/entity/entity-object-template';
import { getObjectBaseFile } from '@/cli/utilities/entity/entity-object-template';
describe('getNewObjectFileContent', () => {
it('should return proper object file using defineObject', () => {
const result = getNewObjectFileContent({
const result = getObjectBaseFile({
data: {
nameSingular: 'company',
namePlural: 'companies',
@@ -28,7 +28,7 @@ describe('getNewObjectFileContent', () => {
});
it('should generate unique UUIDs for each object', () => {
const result1 = getNewObjectFileContent({
const result1 = getObjectBaseFile({
data: {
nameSingular: 'company',
namePlural: 'companies',
@@ -38,7 +38,7 @@ describe('getNewObjectFileContent', () => {
name: 'company',
});
const result2 = getNewObjectFileContent({
const result2 = getObjectBaseFile({
data: {
nameSingular: 'person',
namePlural: 'people',
@@ -1,6 +1,6 @@
import { v4 } from 'uuid';
export const getNewObjectFileContent = ({
export const getObjectBaseFile = ({
data,
}: {
data: {