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

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
defineApplicationConfigure application metadata (required, one per app)
defineObjectDefine custom objects with fields
defineLogicFunctionDefine logic functions with handlers
definePreInstallLogicFunctionDefine a pre-install logic function (one per app)
definePostInstallLogicFunctionDefine a post-install logic function (one per app)
defineFrontComponentDefine front components for custom UI
defineRoleConfigure role permissions and object access
defineFieldExtend existing objects with additional fields
defineViewDefine saved views for objects
defineNavigationMenuItemDefine sidebar navigation links
defineSkillDefine AI agent skills
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 twenty 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 id, name, createdAt, updatedAt, createdBy, updatedBy and deletedAt. You don’t need to define these in your fields array — only add your custom fields. You can override default fields by defining a field with the same name in your fields array, but this is not recommended.

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.
  • (Optional) pre-install function: a logic function that runs before the app is installed.
  • (Optional) post-install function: a logic function that runs after the app is installed.
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).
  • Pre-install and post-install functions are automatically detected during the manifest build. See Pre-install functions and Post-install functions.

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 { CoreApiClient, type Person } from 'twenty-sdk/generated';

const handler = async (params: RoutePayload) => {
  const client = new CoreApiClient();
  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.

Pre-install functions

A pre-install function is a logic function that runs automatically before your app is installed on a workspace. This is useful for validation tasks, prerequisite checks, or preparing workspace state before the main installation proceeds. When you scaffold a new app with create-twenty-app, a pre-install function is generated for you at src/logic-functions/pre-install.ts:
// src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';

const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
  console.log('Pre install logic function executed successfully!', payload.previousVersion);
};

export default definePreInstallLogicFunction({
  universalIdentifier: '<generated-uuid>',
  name: 'pre-install',
  description: 'Runs before installation to prepare the application.',
  timeoutSeconds: 300,
  handler,
});
You can also manually execute the pre-install function at any time using the CLI:
yarn twenty function:execute --preInstall
Key points:
  • Pre-install functions use definePreInstallLogicFunction() — a specialized variant that omits trigger settings (cronTriggerSettings, databaseEventTriggerSettings, httpRouteTriggerSettings, isTool).
  • The handler receives an InstallLogicFunctionPayload with { previousVersion: string } — the version of the app that was previously installed (or an empty string for fresh installs).
  • Only one pre-install function is allowed per application. The manifest build will error if more than one is detected.
  • The function’s universalIdentifier is automatically set as preInstallLogicFunctionUniversalIdentifier on the application manifest during the build — you do not need to reference it in defineApplication().
  • The default timeout is set to 300 seconds (5 minutes) to allow for longer preparation tasks.
  • Pre-install functions do not need triggers — they are invoked by the platform before installation or manually via function:execute --preInstall.

Post-install functions

A post-install function is a logic function that runs automatically after your app is installed on a workspace. This is useful for one-time setup tasks such as seeding default data, creating initial records, or configuring workspace settings. When you scaffold a new app with create-twenty-app, a post-install function is generated for you at src/logic-functions/post-install.ts:
// src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';

const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
  console.log('Post install logic function executed successfully!', payload.previousVersion);
};

export default definePostInstallLogicFunction({
  universalIdentifier: '<generated-uuid>',
  name: 'post-install',
  description: 'Runs after installation to set up the application.',
  timeoutSeconds: 300,
  handler,
});
You can also manually execute the post-install function at any time using the CLI:
yarn twenty function:execute --postInstall
Key points:
  • Post-install functions use definePostInstallLogicFunction() — a specialized variant that omits trigger settings (cronTriggerSettings, databaseEventTriggerSettings, httpRouteTriggerSettings, isTool).
  • The handler receives an InstallLogicFunctionPayload with { previousVersion: string } — the version of the app that was previously installed (or an empty string for fresh installs).
  • Only one post-install function is allowed per application. The manifest build will error if more than one is detected.
  • The function’s universalIdentifier is automatically set as postInstallLogicFunctionUniversalIdentifier on the application manifest during the build — you do not need to reference it in defineApplication().
  • The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding.
  • Post-install functions do not need triggers — they are invoked by the platform during installation or manually via function:execute --postInstall.

Route trigger payload

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 twenty 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.

Marking a logic function as a tool

Logic functions can be exposed as tools for AI agents and workflows. When a function is marked as a tool, it becomes discoverable by Twenty’s AI features and can be selected as a step in workflow automations. To mark a logic function as a tool, set isTool: true and provide a toolInputSchema describing the expected input parameters using JSON Schema:
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-sdk/generated';

const handler = async (params: { companyName: string; domain?: string }) => {
  const client = new CoreApiClient();

  const result = await client.mutation({
    createTask: {
      __args: {
        data: {
          title: `Enrich data for ${params.companyName}`,
          body: `Domain: ${params.domain ?? 'unknown'}`,
        },
      },
      id: true,
    },
  });

  return { taskId: result.createTask.id };
};

export default defineLogicFunction({
  universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
  name: 'enrich-company',
  description: 'Enrich a company record with external data',
  timeoutSeconds: 10,
  handler,
  isTool: true,
  toolInputSchema: {
    type: 'object',
    properties: {
      companyName: {
        type: 'string',
        description: 'The name of the company to enrich',
      },
      domain: {
        type: 'string',
        description: 'The company website domain (optional)',
      },
    },
    required: ['companyName'],
  },
});
Key points:
  • isTool (boolean, default: false): When set to true, the function is registered as a tool and becomes available to AI agents and workflow automations.
  • toolInputSchema (object, optional): A JSON Schema object that describes the parameters your function accepts. AI agents use this schema to understand what inputs the tool expects and to validate calls. If omitted, the schema defaults to { type: 'object', properties: {} } (no parameters).
  • Functions with isTool: false (or unset) are not exposed as tools. They can still be executed directly or called by other functions, but will not appear in tool discovery.
  • Tool naming: When exposed as a tool, the function name is automatically normalized to logic_function_<name> (lowercased, non-alphanumeric characters replaced with underscores). For example, enrich-company becomes logic_function_enrich_company.
  • You can combine isTool with triggers — a function can be both a tool (callable by AI agents) and triggered by events (cron, database events, routes) at the same time.
Write a good description. AI agents rely on the function’s description field to decide when to use the tool. Be specific about what the tool does and when it should be called.

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/front-components/my-widget.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.
  • The component field references your React component.
  • Components are built and synced automatically during yarn twenty app:dev.
You can create new front components in two ways:
  • Scaffolded: Run yarn twenty entity:add and choose the option to add a new front component.
  • Manual: Create a new .tsx file and use defineFrontComponent(), following the same pattern.

Skills

Skills define reusable instructions and capabilities that AI agents can use within your workspace. Use defineSkill() to define skills with built-in validation:
// src/skills/example-skill.ts
import { defineSkill } from 'twenty-sdk';

export default defineSkill({
  universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
  name: 'sales-outreach',
  label: 'Sales Outreach',
  description: 'Guides the AI agent through a structured sales outreach process',
  icon: 'IconBrain',
  content: `You are a sales outreach assistant. When reaching out to a prospect:
1. Research the company and recent news
2. Identify the prospect's role and likely pain points
3. Draft a personalized message referencing specific details
4. Keep the tone professional but conversational`,
});
Key points:
  • name is a unique identifier string for the skill (kebab-case recommended).
  • label is the human-readable display name shown in the UI.
  • content contains the skill instructions — this is the text the AI agent uses.
  • icon (optional) sets the icon displayed in the UI.
  • description (optional) provides additional context about the skill’s purpose.
You can create new skills in two ways:
  • Scaffolded: Run yarn twenty entity:add and choose the option to add a new skill.
  • Manual: Create a new file and use defineSkill(), following the same pattern.

Generated typed clients

Two typed clients are auto-generated by yarn twenty app:dev and stored in node_modules/twenty-sdk/generated based on your workspace schema:
  • CoreApiClient — queries the /graphql endpoint for workspace data
  • MetadataApiClient — queries the /metadata endpoint for workspace configuration and file uploads
import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/generated';

const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });

const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
Both clients are re-generated automatically by yarn twenty app:dev whenever your objects or fields change.

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.

Uploading files

The generated MetadataApiClient includes an uploadFile method for attaching files to file-type fields on your workspace objects. Because standard GraphQL clients do not support multipart file uploads natively, the client provides this dedicated method that implements the GraphQL multipart request specification under the hood.
import { MetadataApiClient } from 'twenty-sdk/generated';
import * as fs from 'fs';

const metadataClient = new MetadataApiClient();

const fileBuffer = fs.readFileSync('./invoice.pdf');

const uploadedFile = await metadataClient.uploadFile(
  fileBuffer,                                         // file contents as a Buffer
  'invoice.pdf',                                      // filename
  'application/pdf',                                  // MIME type (defaults to 'application/octet-stream')
  '58a0a314-d7ea-4865-9850-7fb84e72f30b',            // field universal identifier
);

console.log(uploadedFile);
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
The method signature:
uploadFile(
  fileBuffer: Buffer,
  filename: string,
  contentType: string,
  fieldMetadataUniversalIdentifier: string,
): Promise<{ id: string; path: string; size: number; createdAt: string; url: string }>
ParameterTypeDescription
fileBufferBufferThe raw file contents
filenamestringThe name of the file (used for storage and display)
contentTypestringMIME type of the file (defaults to application/octet-stream if omitted)
fieldMetadataUniversalIdentifierstringThe universalIdentifier of the file-type field on your object
Key points:
  • The uploadFile method is available on MetadataApiClient because the upload mutation is resolved by the /metadata endpoint.
  • It uses the field’s universalIdentifier (not its workspace-specific ID), so your upload code works across any workspace where your app is installed — consistent with how apps reference fields everywhere else.
  • The returned url is a signed URL you can use to access the uploaded file.

Hello World example

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