Skip to main content
Apps are currently in alpha testing. The feature is functional but still evolving.

What Are Apps?

Apps let you build and manage Twenty customizations as code. Instead of configuring everything through the UI, you define your data model and logic functions in code — making it faster to build, maintain, and roll out to multiple workspaces. What you can do today:
  • Define custom objects and fields as code (managed data model)
  • Build logic functions with custom triggers
  • Deploy the same app across multiple workspaces

Prerequisites

Getting Started

Create a new app using the official scaffolder, then authenticate and start developing:
# Scaffold a new app
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app

# If you don't use yarn@4
corepack enable
yarn install

# Authenticate using your API key (you'll be prompted)
yarn auth:login

# Start dev mode: automatically syncs local changes to your workspace
yarn app:dev
From here you can:
# Add a new entity to your application (guided)
yarn entity:add

# Generate a typed Twenty client and workspace entity types
yarn app:generate

# Watch your application's function logs
yarn function:logs

# Execute a function by name
yarn function:execute -n my-function -p '{"name": "test"}'

# Uninstall the application from the current workspace
yarn app:uninstall

# Display commands' help
yarn help
See also: the CLI reference pages for create-twenty-app and twenty-sdk CLI.

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:
my-twenty-app/
  package.json
  yarn.lock
  .gitignore
  .nvmrc
  .yarnrc.yml
  .yarn/
    install-state.gz
  eslint.config.mjs
  tsconfig.json
  README.md
  public/                           # Public assets folder (images, fonts, etc.)
  src/
  ├── application-config.ts           # Required - main application configuration
  ├── roles/
  │   └── default-role.ts               # Default role for logic functions
  ├── logic-functions/
  │   └── hello-world.ts                # Example logic function
  └── front-components/
      └── hello-world.tsx               # Example front component
At a high level:
  • 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 authentication commands 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.
  • public/: A folder for storing public assets (images, fonts, static files) that will be served with your application. Files placed here are uploaded during sync and accessible at runtime.
  • src/: The main place where you define your application-as-code

Entity detection

The SDK detects entities by parsing your TypeScript files for export default define<Entity>({...}) calls. Each entity type has a corresponding helper function exported from twenty-sdk:
Helper functionEntity type
defineObject()Custom object definitions
defineLogicFunction()Logic function definitions
defineFrontComponent()Front component definitions
defineRole()Role definitions
defineField()Field extensions for existing objects
File naming is flexible. Entity detection is AST-based — the SDK scans your source files for the export default define<Entity>({...}) pattern. You can organize your files and folders however you like. Grouping by entity type (e.g., logic-functions/, roles/) is just a convention for code organization, not a requirement.
Example of a detected entity:
// This file can be named anything and placed anywhere in src/
import { defineObject, FieldType } from 'twenty-sdk';

export default defineObject({
  universalIdentifier: '...',
  nameSingular: 'postCard',
  // ... rest of config
});
Later commands will add more files and folders:
  • yarn app:generate will create a generated/ folder (typed Twenty client + workspace types).
  • yarn entity:add will add entity definition files under src/ for your custom objects, functions, front components, or roles.

Authentication

The first time you run yarn auth:login, you’ll be prompted for: Your credentials are stored per-user in ~/.twenty/config.json. You can maintain multiple profiles and switch between them.

Managing workspaces

# Login interactively (recommended)
yarn auth:login

# Login to a specific workspace profile
yarn auth:login --workspace my-custom-workspace

# List all configured workspaces
yarn auth:list

# Switch the default workspace (interactive)
yarn auth:switch

# Switch to a specific workspace
yarn auth:switch production

# Check current authentication status
yarn auth:status
Once you’ve switched workspaces with auth:switch, all subsequent commands will use that workspace by default. You can still override it temporarily with --workspace <name>.

Use the SDK resources (types & config)

The twenty-sdk provides typed building blocks and helper functions you use inside your app. Below are the key pieces you’ll touch most often.

Helper functions

The SDK provides helper functions for defining your app entities. As described in Entity detection, you must use export default define<Entity>({...}) for your entities to be detected:
FunctionPurpose
defineApplication()Configure application metadata (required, one per app)
defineObject()Define custom objects with fields
defineLogicFunction()Define logic functions with handlers
defineFrontComponent()Define front components for custom UI
defineRole()Configure role permissions and object access
defineField()Extend existing objects with additional fields
These functions validate your configuration at build time and provide IDE autocompletion and type safety.

Defining objects

Custom objects describe both schema and behavior for records in your workspace. Use defineObject() to define objects with built-in validation:
// src/app/postCard.object.ts
import { defineObject, FieldType } from 'twenty-sdk';

enum PostCardStatus {
  DRAFT = 'DRAFT',
  SENT = 'SENT',
  DELIVERED = 'DELIVERED',
  RETURNED = 'RETURNED',
}

export default defineObject({
  universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
  nameSingular: 'postCard',
  namePlural: 'postCards',
  labelSingular: 'Post Card',
  labelPlural: 'Post Cards',
  description: 'A post card object',
  icon: 'IconMail',
  fields: [
    {
      universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
      name: 'content',
      type: FieldType.TEXT,
      label: 'Content',
      description: "Postcard's content",
      icon: 'IconAbc',
    },
    {
      universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
      name: 'recipientName',
      type: FieldType.FULL_NAME,
      label: 'Recipient name',
      icon: 'IconUser',
    },
    {
      universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
      name: 'recipientAddress',
      type: FieldType.ADDRESS,
      label: 'Recipient address',
      icon: 'IconHome',
    },
    {
      universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
      name: 'status',
      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' },
      ],
    },
    {
      universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
      name: 'deliveredAt',
      type: FieldType.DATE_TIME,
      label: 'Delivered at',
      icon: 'IconCheck',
      isNullable: true,
      defaultValue: null,
    },
  ],
});
Key points:
  • Use defineObject() for built-in validation and better IDE support.
  • 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 entity:add, which guides you through naming, fields, and relationships.
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.

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.
Use defineApplication() to define your application configuration:
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';

export default defineApplication({
  universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
  displayName: 'My Twenty App',
  description: 'My first Twenty app',
  icon: 'IconWorld',
  applicationVariables: {
    DEFAULT_RECIPIENT_NAME: {
      universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
      description: 'Default recipient name for postcards',
      value: 'Jane Doe',
      isSecret: false,
    },
  },
  defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});
Notes:
  • universalIdentifier fields are deterministic IDs you own; generate them once and keep them stable across syncs.
  • applicationVariables become environment variables for your functions (for example, DEFAULT_RECIPIENT_NAME is available as process.env.DEFAULT_RECIPIENT_NAME).
  • defaultRoleUniversalIdentifier must match the role file (see below).

Roles and permissions

Applications can define roles that encapsulate permissions on your workspace’s objects and actions. The field defaultRoleUniversalIdentifier in application-config.ts designates the default role used by your app’s logic 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.ts)
When you scaffold a new app, the CLI also creates a default role file. Use defineRole() to define roles with built-in validation:
// src/roles/default-role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk';

export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
  'b648f87b-1d26-4961-b974-0908fd991061';

export default defineRole({
  universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
  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: [
    {
      objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
      canReadObjectRecords: true,
      canUpdateObjectRecords: true,
      canSoftDeleteObjectRecords: false,
      canDestroyObjectRecords: false,
    },
  ],
  fieldPermissions: [
    {
      objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
      fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
      canReadFieldValue: false,
      canUpdateFieldValue: false,
    },
  ],
  permissionFlags: [PermissionFlag.APPLICATIONS],
});
The universalIdentifier of this role is then referenced in application-config.ts as defaultRoleUniversalIdentifier. In other words:
  • *.role.ts defines what the default function role can do.
  • application-config.ts points to that role so your functions inherit its permissions.
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.

Logic function config and entrypoint

Each function file uses defineLogicFunction() to export a configuration with a handler and optional triggers.
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';

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'
    : 'Hello world';

  const result = await client.mutation({
    createPostCard: {
      __args: { data: { name } },
      id: true,
      name: true,
    },
  });
  return result;
};

export default defineLogicFunction({
  universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
  name: 'create-new-post-card',
  timeoutSeconds: 2,
  handler,
  triggers: [
    // Public HTTP route trigger '/s/post-card/create'
    {
      universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
      type: 'route',
      path: '/post-card/create',
      httpMethod: 'GET',
      isAuthRequired: false,
    },
    // Cron trigger (CRON pattern)
    // {
    //   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'],
    // },
  ],
});
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. When the event operation is updated, specific fields to listen to can be specified in the updatedFields array. If left undefined or empty, any update will trigger the function.
e.g. person.updated
Notes:
  • The triggers array is optional. Functions without triggers can be used as utility functions called by other functions.
  • You can mix multiple trigger types in a single function.

Route trigger payload

Breaking change (v1.16, January 2026): The route trigger payload format has changed. Prior to v1.16, query parameters, path parameters, and body were sent directly as the payload. Starting with v1.16, they are nested inside a structured RoutePayload object.Before v1.16:
const handler = async (params) => {
  const { param1, param2 } = params; // Direct access
};
After v1.16:
const handler = async (event: RoutePayload) => {
  const { param1, param2 } = event.body; // Access via .body
  const { queryParam } = event.queryStringParameters;
  const { id } = event.pathParameters;
};
To migrate existing functions: Update your handler to destructure from event.body, event.queryStringParameters, or event.pathParameters instead of directly from the params object.
When a route trigger invokes your logic function, it receives a RoutePayload object that follows the AWS HTTP API v2 format. Import the type from twenty-sdk:
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';

const handler = async (event: RoutePayload) => {
  // Access request data
  const { headers, queryStringParameters, pathParameters, body } = event;

  // HTTP method and path are available in requestContext
  const { method, path } = event.requestContext.http;

  return { message: 'Success' };
};
The RoutePayload type has the following structure:
PropertyTypeDescription
headersRecord<string, string | undefined>HTTP headers (only those listed in forwardedRequestHeaders)
queryStringParametersRecord<string, string | undefined>Query string parameters (multiple values joined with commas)
pathParametersRecord<string, string | undefined>Path parameters extracted from the route pattern (e.g., /users/:id{ id: '123' })
bodyobject | nullParsed request body (JSON)
isBase64EncodedbooleanWhether the body is base64 encoded
requestContext.http.methodstringHTTP method (GET, POST, PUT, PATCH, DELETE)
requestContext.http.pathstringRaw request path

Forwarding HTTP headers

By default, HTTP headers from incoming requests are not passed to your logic function for security reasons. To access specific headers, explicitly list them in the forwardedRequestHeaders array:
export default defineLogicFunction({
  universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
  name: 'webhook-handler',
  handler,
  triggers: [
    {
      universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
      type: 'route',
      path: '/webhook',
      httpMethod: 'POST',
      isAuthRequired: false,
      forwardedRequestHeaders: ['x-webhook-signature', 'content-type'],
    },
  ],
});
In your handler, you can then access these headers:
const handler = async (event: RoutePayload) => {
  const signature = event.headers['x-webhook-signature'];
  const contentType = event.headers['content-type'];

  // Validate webhook signature...
  return { received: true };
};
Header names are normalized to lowercase. Access them using lowercase keys (for example, event.headers['content-type']).
You can create new functions in two ways:
  • Scaffolded: Run yarn entity:add and choose the option to add a new logic function. This generates a starter file with a handler and config.
  • Manual: Create a new *.logic-function.ts file and use defineLogicFunction(), following the same pattern.

Front components

Front components let you build custom React components that render within Twenty’s UI. Use defineFrontComponent() to define components with built-in validation:
// src/my-widget.front-component.tsx
import { defineFrontComponent } from 'twenty-sdk';

const MyWidget = () => {
  return (
    <div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
      <h1>My Custom Widget</h1>
      <p>This is a custom front component for Twenty.</p>
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
  name: 'my-widget',
  description: 'A custom widget component',
  component: MyWidget,
});
Key points:
  • Front components are React components that render in isolated contexts within Twenty.
  • Use the *.front-component.tsx file suffix for automatic detection.
  • The component field references your React component.
  • Components are built and synced automatically during yarn app:dev.
You can create new front components in two ways:
  • Scaffolded: Run yarn entity:add and choose the option to add a new front component.
  • Manual: Create a new *.front-component.tsx file and use defineFrontComponent().

Generated typed client

Run yarn app:generate to create a local typed client in generated/ based on your workspace schema. Use it in your functions:
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 or when onboarding to a new workspace.

Runtime credentials in logic 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 defaultRoleUniversalIdentifier. This is the default role used by logic functions of your application.
  • Applications can define roles to follow least‑privilege. Grant only the permissions your functions need, then point defaultRoleUniversalIdentifier to that role’s universal identifier.

Hello World example

Explore a minimal, end-to-end example that demonstrates objects, logic functions, front components, and multiple triggers here:

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:
yarn add -D twenty-sdk
Then add scripts like these:
{
  "scripts": {
    "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: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.
  • 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