Saltar para o conteúdo principal
Os aplicativos estão atualmente em testes alfa. O recurso é funcional, mas ainda está evoluindo.
O pacote twenty-sdk fornece blocos de construção tipados para criar seu app. Esta página cobre todos os tipos de entidade e clientes de API disponíveis no SDK.

Funções DefineEntity

O SDK fornece funções para definir as entidades do seu app. Você deve usar export default defineEntity({...}) para que o SDK detecte suas entidades. Essas funções validam sua configuração em tempo de compilação e oferecem autocompletar na IDE e segurança de tipos.
A organização de arquivos fica a seu critério. A detecção de entidades é baseada em AST — o SDK encontra chamadas a export default defineEntity(...) independentemente de onde o arquivo esteja. Agrupar arquivos por tipo (por exemplo, logic-functions/, roles/) é apenas uma convenção, não um requisito.
Papéis encapsulam permissões sobre os objetos e ações do seu espaço de trabalho.
restricted-company-role.ts
import {
  defineRole,
  PermissionFlag,
  STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';

export default defineRole({
  universalIdentifier: '2c80f640-2083-4803-bb49-003e38279de6',
  label: 'My new role',
  description: 'A role that can be used in your workspace',
  canReadAllObjectRecords: false,
  canUpdateAllObjectRecords: false,
  canSoftDeleteAllObjectRecords: false,
  canDestroyAllObjectRecords: false,
  canUpdateAllSettings: false,
  canBeAssignedToAgents: false,
  canBeAssignedToUsers: false,
  canBeAssignedToApiKeys: false,
  objectPermissions: [
    {
      objectUniversalIdentifier:
        STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
      canReadObjectRecords: true,
      canUpdateObjectRecords: true,
      canSoftDeleteObjectRecords: false,
      canDestroyObjectRecords: false,
    },
  ],
  fieldPermissions: [
    {
      objectUniversalIdentifier:
        STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
      fieldUniversalIdentifier:
        STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier,
      canReadFieldValue: false,
      canUpdateFieldValue: false,
    },
  ],
  permissionFlags: [PermissionFlag.APPLICATIONS],
});
Todo app deve ter exatamente uma chamada a defineApplication que descreve:
  • Identidade: identificadores, nome de exibição e descrição.
  • Permissões: qual papel é usado por suas funções e componentes de front-end.
  • Variáveis (opcional): pares chave–valor expostos às suas funções como variáveis de ambiente.
  • (Opcional) Funções de pré-instalação/pós-instalação: funções de lógica que são executadas antes ou depois da instalação.
src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';

export default defineApplication({
  universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
  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,
});
Notas:
  • Os campos universalIdentifier são IDs determinísticos que você controla. Gere-os uma vez e mantenha-os estáveis entre sincronizações.
  • applicationVariables tornam-se variáveis de ambiente para suas funções e componentes de front-end (por exemplo, DEFAULT_RECIPIENT_NAME fica disponível como process.env.DEFAULT_RECIPIENT_NAME).
  • defaultRoleUniversalIdentifier deve fazer referência a um papel definido com defineRole() (veja acima).
  • As funções de pré-instalação e pós-instalação são detectadas automaticamente durante a construção do manifesto — você não precisa referenciá-las em defineApplication().

Metadados do Marketplace

Se você planeja publicar seu app, estes campos opcionais controlam como seu app aparece no marketplace:
CampoDescrição
autorNome do autor ou da empresa
categoriaCategoria do app para filtragem no marketplace
logoUrlCaminho para o logo do seu app (por exemplo, public/logo.png)
screenshotsArray de caminhos de capturas de tela (por exemplo, public/screenshot-1.png)
aboutDescriptionDescrição em markdown mais longa para a aba “Sobre”. Se omitido, o marketplace usa o README.md do pacote no npm
websiteUrlLink para seu site
termsUrlLink para os Termos de Serviço
emailSupportEndereço de e-mail de suporte
issueReportUrlLink para o rastreador de problemas

Papéis e permissões

O campo defaultRoleUniversalIdentifier em application-config.ts designa o papel padrão usado pelas funções de lógica e pelos componentes de front-end do seu app. Veja defineRole acima para detalhes.
  • O token em tempo de execução injetado como TWENTY_APP_ACCESS_TOKEN é derivado desse papel.
  • O cliente tipado é restrito às permissões concedidas a esse papel.
  • Siga o princípio do menor privilégio: crie um papel dedicado com apenas as permissões de que suas funções precisam.
Papel de função padrão
Ao criar um novo app com o scaffold, a CLI cria um arquivo de papel padrão:
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: true,
  canUpdateAllObjectRecords: false,
  canSoftDeleteAllObjectRecords: false,
  canDestroyAllObjectRecords: false,
  canUpdateAllSettings: false,
  canBeAssignedToAgents: false,
  canBeAssignedToUsers: false,
  canBeAssignedToApiKeys: false,
  objectPermissions: [],
  fieldPermissions: [],
  permissionFlags: [],
});
O universalIdentifier desse papel é referenciado em application-config.ts como defaultRoleUniversalIdentifier:
  • *.role.ts define o que o papel pode fazer.
  • application-config.ts aponta para esse papel para que suas funções herdem suas permissões.
Notas:
  • Comece pelo papel gerado pelo scaffold e depois restrinja-o progressivamente seguindo o princípio do menor privilégio.
  • Substitua objectPermissions e fieldPermissions pelos objetos e campos de que suas funções realmente precisam.
  • permissionFlags controlam o acesso a recursos em nível de plataforma. Mantenha-os no mínimo necessário.
  • Veja um exemplo funcional: hello-world/src/roles/function-role.ts.
Objetos personalizados descrevem tanto o esquema quanto o comportamento de registros no seu espaço de trabalho. Use defineObject() para definir objetos com validação integrada:
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,
    },
  ],
});
Pontos-chave:
  • Use defineObject() para validação integrada e melhor suporte na IDE.
  • O universalIdentifier deve ser exclusivo e estável entre implantações.
  • Cada campo requer name, type, label e seu próprio universalIdentifier estável.
  • O array fields é opcional — você pode definir objetos sem campos personalizados.
  • Você pode criar novos objetos usando yarn twenty add, que orienta você sobre nomeação, campos e relacionamentos.
Os campos base são criados automaticamente. Quando você define um objeto personalizado, o Twenty adiciona automaticamente campos padrão como id, name, createdAt, updatedAt, createdBy, updatedBy e deletedAt. Você não precisa definir esses no seu array fields — adicione apenas seus campos personalizados. Você pode substituir os campos padrão definindo um campo com o mesmo nome no seu array fields, mas isso não é recomendado.
Use defineField() para adicionar campos a objetos que não são seus — como objetos padrão do Twenty (Person, Company, etc.). ou a objetos de outros apps. Ao contrário dos campos inline em defineObject(), os campos independentes exigem um objectUniversalIdentifier para especificar qual objeto eles estendem:
src/fields/company-loyalty-tier.field.ts
import { defineField, FieldType } from 'twenty-sdk';

export default defineField({
  universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890',
  objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object
  name: 'loyaltyTier',
  type: FieldType.SELECT,
  label: 'Loyalty Tier',
  icon: 'IconStar',
  options: [
    { value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' },
    { value: 'SILVER', label: 'Silver', position: 1, color: 'gray' },
    { value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' },
  ],
});
Pontos-chave:
  • objectUniversalIdentifier identifica o objeto de destino. Para objetos padrão, use STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS exportado de twenty-sdk.
  • Ao definir campos inline em defineObject(), você não precisa de objectUniversalIdentifier — ele é herdado do objeto pai.
  • defineField() é a única forma de adicionar campos a objetos que você não criou com defineObject().
As relações conectam objetos entre si. No Twenty, as relações são sempre bidirecionais — você define ambos os lados, e cada lado faz referência ao outro.Existem dois tipos de relação:
Tipo de relaçãoDescriçãoTem chave estrangeira?
MANY_TO_ONEMuitos registros deste objeto apontam para um registro do destinoSim (joinColumnName)
ONE_TO_MANYUm registro deste objeto possui muitos registros do destinoNão (lado inverso)

Como as relações funcionam

Toda relação requer dois campos que façam referência um ao outro:
  1. O lado MANY_TO_ONE — fica no objeto que contém a chave estrangeira
  2. O lado ONE_TO_MANY — fica no objeto que possui a coleção
Ambos os campos usam FieldType.RELATION e fazem referência cruzada um ao outro via relationTargetFieldMetadataUniversalIdentifier.

Exemplo: Um cartão postal tem muitos destinatários

Suponha que um PostCard possa ser enviado para muitos registros PostCardRecipient. Cada destinatário pertence a exatamente um cartão postal.Etapa 1: Defina o lado ONE_TO_MANY em PostCard (o lado “um”):
src/fields/post-card-recipients-on-post-card.field.ts
import { defineField, FieldType, RelationType } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';

// Export so the other side can reference it
export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111';
// Import from the other side
import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field';

export default defineField({
  universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
  objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
  type: FieldType.RELATION,
  name: 'postCardRecipients',
  label: 'Post Card Recipients',
  icon: 'IconUsers',
  relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
  relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
  universalSettings: {
    relationType: RelationType.ONE_TO_MANY,
  },
});
Etapa 2: Defina o lado MANY_TO_ONE em PostCardRecipient (o lado “muitos” — contém a chave estrangeira):
src/fields/post-card-on-post-card-recipient.field.ts
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';

// Export so the other side can reference it
export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222';
// Import from the other side
import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field';

export default defineField({
  universalIdentifier: POST_CARD_FIELD_ID,
  objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
  type: FieldType.RELATION,
  name: 'postCard',
  label: 'Post Card',
  icon: 'IconMail',
  relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
  relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
  universalSettings: {
    relationType: RelationType.MANY_TO_ONE,
    onDelete: OnDeleteAction.CASCADE,
    joinColumnName: 'postCardId',
  },
});
Importações circulares: Ambos os campos de relação referenciam o universalIdentifier um do outro. Para evitar problemas de importação circular, exporte os IDs dos seus campos como constantes nomeadas de cada arquivo e importe-os no outro arquivo. O sistema de build resolve isso em tempo de compilação.

Relacionando a objetos padrão

Para criar uma relação com um objeto integrado do Twenty (Person, Company, etc.), use STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS:
src/fields/person-on-self-hosting-user.field.ts
import {
  defineField,
  FieldType,
  RelationType,
  OnDeleteAction,
  STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object';

export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333';
export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444';

export default defineField({
  universalIdentifier: PERSON_FIELD_ID,
  objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER,
  type: FieldType.RELATION,
  name: 'person',
  label: 'Person',
  description: 'Person matching with the self hosting user',
  isNullable: true,
  relationTargetObjectMetadataUniversalIdentifier:
    STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
  relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID,
  universalSettings: {
    relationType: RelationType.MANY_TO_ONE,
    onDelete: OnDeleteAction.SET_NULL,
    joinColumnName: 'personId',
  },
});

Propriedades de campos de relação

PropriedadeObrigatórioDescrição
tipoSimDeve ser FieldType.RELATION
relationTargetObjectMetadataUniversalIdentifierSimO universalIdentifier do objeto de destino
relationTargetFieldMetadataUniversalIdentifierSimO universalIdentifier do campo correspondente no objeto de destino
universalSettings.relationTypeSimRelationType.MANY_TO_ONE ou RelationType.ONE_TO_MANY
universalSettings.onDeleteApenas para MANY_TO_ONEO que acontece quando o registro referenciado é excluído: CASCADE, SET_NULL, RESTRICT ou NO_ACTION
universalSettings.joinColumnNameApenas para MANY_TO_ONENome da coluna no banco de dados para a chave estrangeira (por exemplo, postCardId)

Campos de relação inline em defineObject

Você também pode definir campos de relação diretamente dentro de defineObject(). Nesse caso, omita objectUniversalIdentifier — ele é herdado do objeto pai:
export default defineObject({
  universalIdentifier: '...',
  nameSingular: 'postCardRecipient',
  // ...
  fields: [
    {
      universalIdentifier: POST_CARD_FIELD_ID,
      type: FieldType.RELATION,
      name: 'postCard',
      label: 'Post Card',
      relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
      relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
      universalSettings: {
        relationType: RelationType.MANY_TO_ONE,
        onDelete: OnDeleteAction.CASCADE,
        joinColumnName: 'postCardId',
      },
    },
    // ... other fields
  ],
});
Cada arquivo de função usa defineLogicFunction() para exportar uma configuração com um handler e gatilhos opcionais.
src/logic-functions/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import { CoreApiClient, type Person } from 'twenty-client-sdk/core';

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,
  httpRouteTriggerSettings: {
    path: '/post-card/create',
    httpMethod: 'GET',
    isAuthRequired: false,
  },
  /*databaseEventTriggerSettings: {
    eventName: 'people.created',
  },*/
  /*cronTriggerSettings: {
    pattern: '0 0 1 1 *',
  },*/
});
Tipos de gatilho disponíveis:
  • httpRoute: Expõe sua função em um caminho e método HTTP no endpoint /s/:
por exemplo, path: '/post-card/create' é acessível em https://your-twenty-server.com/s/post-card/create
  • cron: Executa sua função em um agendamento usando uma expressão CRON.
  • databaseEvent: Executa em eventos do ciclo de vida de objetos do espaço de trabalho. Quando a operação do evento é updated, campos específicos a serem observados podem ser especificados no array updatedFields. Se deixar indefinido ou vazio, qualquer atualização acionará a função.
por exemplo, person.updated, *.created, company.*
Você também pode executar manualmente uma função usando a CLI:
yarn twenty exec -n create-new-post-card -p '{"key": "value"}'
yarn twenty exec -y e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
Você pode acompanhar os logs com:
yarn twenty logs

Payload de gatilho de rota

Quando um gatilho de rota invoca sua função de lógica, ela recebe um objeto RoutePayload que segue o formato HTTP API v2 da AWS. Importe o tipo RoutePayload de twenty-sdk:
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';

const handler = async (event: RoutePayload) => {
  const { headers, queryStringParameters, pathParameters, body } = event;
  const { method, path } = event.requestContext.http;

  return { message: 'Success' };
};
O tipo RoutePayload tem a seguinte estrutura:
PropriedadeTipoDescriçãoExemplo
headersRecord<string, string | undefined>Cabeçalhos HTTP (apenas aqueles listados em forwardedRequestHeaders)veja a seção abaixo
queryStringParametersRecord<string, string | undefined>Parâmetros de query string (valores múltiplos unidos por vírgulas)/users?ids=1&ids=2&ids=3&name=Alice -> { ids: '1,2,3', name: 'Alice' }
pathParametersRecord<string, string | undefined>Parâmetros de caminho extraídos do padrão de rota/users/:id, /users/123 -> { id: '123' }
bodyobject | nullCorpo da requisição analisado (JSON){ id: 1 } -> { id: 1 }
isBase64EncodedbooleanSe o corpo está codificado em base64
requestContext.http.methodstringMétodo HTTP (GET, POST, PUT, PATCH, DELETE)
requestContext.http.pathstringCaminho bruto da requisição

forwardedRequestHeaders

Por padrão, os cabeçalhos HTTP das requisições recebidas não são repassados para sua função de lógica por motivos de segurança. Para acessar cabeçalhos específicos, liste-os explicitamente no array forwardedRequestHeaders:
export default defineLogicFunction({
  universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
  name: 'webhook-handler',
  handler,
  httpRouteTriggerSettings: {
    path: '/webhook',
    httpMethod: 'POST',
    isAuthRequired: false,
    forwardedRequestHeaders: ['x-webhook-signature', 'content-type'],
  },
});
No seu handler, acesse os cabeçalhos encaminhados assim:
const handler = async (event: RoutePayload) => {
  const signature = event.headers['x-webhook-signature'];
  const contentType = event.headers['content-type'];

  // Validate webhook signature...
  return { received: true };
};
Os nomes dos cabeçalhos são normalizados para minúsculas. Acesse-os usando chaves em minúsculas (por exemplo, event.headers['content-type']).

Expor uma função como ferramenta

Funções lógicas podem ser expostas como ferramentas para agentes de IA e fluxos de trabalho. Quando marcada como ferramenta, uma função fica detectável pelos recursos de IA do Twenty e pode ser usada em automações de fluxos de trabalho.Para marcar uma função de lógica como ferramenta, defina isTool: true:
src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-client-sdk/core';

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,
});
Pontos-chave:
  • Você pode combinar isTool com gatilhos — uma função pode ser ao mesmo tempo uma ferramenta (chamável por agentes de IA) e acionada por eventos.
  • toolInputSchema (opcional): Um objeto JSON Schema que descreve os parâmetros que sua função aceita. O schema é calculado automaticamente a partir da análise estática do código-fonte, mas você pode defini-lo explicitamente:
export default defineLogicFunction({
  ...,
  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'],
  },
});
Escreva uma boa description. Os agentes de IA dependem do campo description da função para decidir quando usar a ferramenta. Seja específico sobre o que a ferramenta faz e quando ela deve ser chamada.
Uma função de pré-instalação é uma função de lógica que é executada automaticamente antes de o seu aplicativo ser instalado em um espaço de trabalho. Isso é útil para tarefas de validação, verificações de pré-requisitos ou para preparar o estado do espaço de trabalho antes que a instalação principal prossiga.
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: 'e0604b9e-e946-456b-886d-3f27d9a6b324',
  name: 'pre-install',
  description: 'Runs before installation to prepare the application.',
  timeoutSeconds: 300,
  handler,
});
Você também pode executar manualmente a função de pré-instalação a qualquer momento usando a CLI:
yarn twenty exec --preInstall
Pontos-chave:
  • As funções de pré-instalação usam definePreInstallLogicFunction() — uma variante especializada que omite as configurações de gatilho (cronTriggerSettings, databaseEventTriggerSettings, httpRouteTriggerSettings, isTool).
  • O manipulador recebe um InstallLogicFunctionPayload com { previousVersion: string } — a versão do app que foi instalada anteriormente (ou uma string vazia para instalações novas).
  • É permitida apenas uma função de pré-instalação por app. A geração do manifesto apresentará erro se mais de uma for detectada.
  • O universalIdentifier da função é definido automaticamente como preInstallLogicFunctionUniversalIdentifier no manifesto do aplicativo durante a geração — você não precisa referenciá-lo em defineApplication().
  • O tempo limite padrão é definido como 300 segundos (5 minutos) para permitir tarefas de preparação mais longas.
Uma função de pós-instalação é uma função de lógica que é executada automaticamente após o seu aplicativo ser instalado em um espaço de trabalho. Isso é útil para tarefas de configuração únicas, como preencher dados padrão, criar registros iniciais ou configurar as configurações do espaço de trabalho.
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: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
  name: 'post-install',
  description: 'Runs after installation to set up the application.',
  timeoutSeconds: 300,
  handler,
});
Você também pode executar manualmente a função de pós-instalação a qualquer momento usando a CLI:
yarn twenty exec --postInstall
Pontos-chave:
  • As funções de pós-instalação usam definePostInstallLogicFunction() — uma variante especializada que omite as configurações de gatilho (cronTriggerSettings, databaseEventTriggerSettings, httpRouteTriggerSettings, isTool).
  • O manipulador recebe um InstallLogicFunctionPayload com { previousVersion: string } — a versão do app que foi instalada anteriormente (ou uma string vazia para instalações novas).
  • É permitida apenas uma função de pós-instalação por app. A geração do manifesto apresentará erro se mais de uma for detectada.
  • O universalIdentifier da função é definido automaticamente como postInstallLogicFunctionUniversalIdentifier no manifesto do aplicativo durante a geração — você não precisa referenciá-lo em defineApplication().
  • O tempo limite padrão é definido como 300 segundos (5 minutos) para permitir tarefas de configuração mais longas, como o pré-carregamento de dados.
Componentes de front-end são componentes React que renderizam diretamente dentro da UI do Twenty. Eles são executados em um Web Worker isolado usando Remote DOM — seu código é sandboxed, mas renderiza nativamente na página, não em um iframe.

Onde os componentes de front-end podem ser usados

Os componentes de front-end podem ser renderizados em dois locais dentro do Twenty:
  • Painel lateral — Componentes de front-end não headless abrem no painel lateral direito. Este é o comportamento padrão quando um componente de front-end é acionado pelo menu de comandos.
  • Widgets (painéis e páginas de registro) — Componentes de front-end podem ser incorporados como widgets nos layouts de página. Ao configurar um painel ou o layout de uma página de registro, os usuários podem adicionar um widget de componente de front-end.

Exemplo básico

A maneira mais rápida de ver um componente de front-end em ação é registrá-lo como um comando. Adicionar um campo command com isPinned: true faz com que ele apareça como um botão de ação rápida no canto superior direito da página — não é necessário layout de página:
src/front-components/hello-world.tsx
import { defineFrontComponent } from 'twenty-sdk';

const HelloWorld = () => {
  return (
    <div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
      <h1>Hello from my app!</h1>
      <p>This component renders inside Twenty.</p>
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
  name: 'hello-world',
  description: 'A simple front component',
  component: HelloWorld,
  command: {
    universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
    shortLabel: 'Hello',
    label: 'Hello World',
    icon: 'IconBolt',
    isPinned: true,
    availabilityType: 'GLOBAL',
  },
});
Após sincronizar com yarn twenty dev, a ação rápida aparece no canto superior direito da página:
Botão de ação rápida no canto superior direito
Clique nele para renderizar o componente inline.

Campos de configuração

CampoObrigatórioDescrição
universalIdentifierSimID único e estável para este componente
componentSimUma função de componente React
nameNãoNome de Exibição
descriptionNãoDescrição do que o componente faz
isHeadlessNãoDefina como true se o componente não tiver interface visível (veja abaixo)
commandNãoRegistre o componente como um comando (veja opções de comando abaixo)

Colocando um componente de front-end em uma página

Além de comandos, você pode incorporar um componente de front-end diretamente em uma página de registro adicionando-o como um widget em um layout de página. Veja a seção definePageLayout para obter detalhes.

Headless vs não headless

Os componentes de front-end têm dois modos de renderização controlados pela opção isHeadless:Não headless (padrão) — O componente renderiza uma interface visível. Quando acionado pelo menu de comandos, ele é aberto no painel lateral. Este é o comportamento padrão quando isHeadless é false ou omitido.Headless (isHeadless: true) — The component mounts invisibly in the background. Ele não abre o painel lateral. Componentes headless são projetados para ações que executam lógica e, em seguida, se desmontam — por exemplo, executar uma tarefa assíncrona, navegar para uma página ou exibir um modal de confirmação. Eles se combinam naturalmente com os componentes Command do SDK descritos abaixo.
src/front-components/sync-tracker.tsx
import { defineFrontComponent, useRecordId, enqueueSnackbar } from 'twenty-sdk';
import { useEffect } from 'react';

const SyncTracker = () => {
  const recordId = useRecordId();

  useEffect(() => {
    enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' });
  }, [recordId]);

  return null;
};

export default defineFrontComponent({
  universalIdentifier: '...',
  name: 'sync-tracker',
  description: 'Tracks record views silently',
  isHeadless: true,
  component: SyncTracker,
});
Como o componente retorna null, o Twenty ignora renderizar um contêiner para ele — nenhum espaço vazio aparece no layout. O componente ainda tem acesso a todos os hooks e à API de comunicação do host.

Componentes Command do SDK

O pacote twenty-sdk fornece quatro componentes auxiliares Command projetados para componentes de front-end headless. Cada componente executa uma ação ao montar, trata erros exibindo uma notificação de snackbar e desmonta automaticamente o componente de front-end ao concluir.Importe-os de twenty-sdk/command:
  • Command — Executa um callback assíncrono via a prop execute.
  • CommandLink — Navega para um caminho do app. Props: to, params, queryParams, options.
  • CommandModal — Abre um modal de confirmação. Se o usuário confirmar, executa o callback execute. Props: title, subtitle, execute, confirmButtonText, confirmButtonAccent.
  • CommandOpenSidePanelPage — Abre uma página específica do painel lateral. Props: page, pageTitle, pageIcon.
Aqui está um exemplo completo de um componente de front-end headless usando Command para executar uma ação a partir do menu de comandos:
src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk';
import { Command } from 'twenty-sdk/command';
import { CoreApiClient } from 'twenty-sdk/clients';

const RunAction = () => {
  const execute = async () => {
    const client = new CoreApiClient();

    await client.mutation({
      createTask: {
        __args: { data: { title: 'Created by my app' } },
        id: true,
      },
    });
  };

  return <Command execute={execute} />;
};

export default defineFrontComponent({
  universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
  name: 'run-action',
  description: 'Creates a task from the command menu',
  component: RunAction,
  isHeadless: true,
  command: {
    universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
    label: 'Run my action',
    icon: 'IconPlayerPlay',
  },
});
E um exemplo usando CommandModal para solicitar confirmação antes de executar:
src/front-components/delete-draft.tsx
import { defineFrontComponent } from 'twenty-sdk';
import { CommandModal } from 'twenty-sdk/command';

const DeleteDraft = () => {
  const execute = async () => {
    // perform the deletion
  };

  return (
    <CommandModal
      title="Delete draft?"
      subtitle="This action cannot be undone."
      execute={execute}
      confirmButtonText="Delete"
      confirmButtonAccent="danger"
    />
  );
};

export default defineFrontComponent({
  universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
  name: 'delete-draft',
  description: 'Deletes a draft with confirmation',
  component: DeleteDraft,
  isHeadless: true,
  command: {
    universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
    label: 'Delete draft',
    icon: 'IconTrash',
  },
});

Acessando o contexto de execução

Dentro do seu componente, use hooks do SDK para acessar o usuário atual, o registro e a instância do componente:
src/front-components/record-info.tsx
import {
  defineFrontComponent,
  useUserId,
  useRecordId,
  useFrontComponentId,
} from 'twenty-sdk';

const RecordInfo = () => {
  const userId = useUserId();
  const recordId = useRecordId();
  const componentId = useFrontComponentId();

  return (
    <div>
      <p>User: {userId}</p>
      <p>Record: {recordId ?? 'No record context'}</p>
      <p>Component: {componentId}</p>
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012',
  name: 'record-info',
  component: RecordInfo,
});
Hooks disponíveis:
HookRetornaDescrição
useUserId()string ou nullO ID do usuário atual
useRecordId()string ou nullO ID do registro atual (quando colocado em uma página de registro)
useFrontComponentId()stringO ID desta instância do componente
useFrontComponentExecutionContext(selector)variaAcesse o contexto de execução completo com uma função seletora

API de comunicação do host

Componentes de front-end podem acionar navegação, modais e notificações usando funções de twenty-sdk:
FunçãoDescrição
navigate(to, params?, queryParams?, options?)Navegar para uma página no app
openSidePanelPage(params)Abrir um painel lateral
closeSidePanel()Fecha o painel lateral
openCommandConfirmationModal(params)Mostrar um diálogo de confirmação
enqueueSnackbar(params)Mostrar uma notificação do tipo toast
unmountFrontComponent()Desmontar o componente
updateProgress(progress)Atualizar um indicador de progresso
Aqui está um exemplo que usa a API do host para exibir um snackbar e fechar o painel lateral após a conclusão de uma ação:
src/front-components/archive-record.tsx
import { defineFrontComponent, useRecordId } from 'twenty-sdk';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-sdk/clients';

const ArchiveRecord = () => {
  const recordId = useRecordId();

  const handleArchive = async () => {
    const client = new CoreApiClient();

    await client.mutation({
      updateTask: {
        __args: { id: recordId, data: { status: 'ARCHIVED' } },
        id: true,
      },
    });

    await enqueueSnackbar({
      message: 'Record archived',
      variant: 'success',
    });

    await closeSidePanel();
  };

  return (
    <div style={{ padding: '20px' }}>
      <p>Archive this record?</p>
      <button onClick={handleArchive}>Archive</button>
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
  name: 'archive-record',
  description: 'Archives the current record',
  component: ArchiveRecord,
});

Opções de comando

Adicionar um campo command a defineFrontComponent registra o componente no menu de comandos (Cmd+K). Se isPinned for true, ele também aparece como um botão de ação rápida no canto superior direito da página.
CampoObrigatórioDescrição
universalIdentifierSimID exclusivo e estável para o comando
labelSimRótulo completo exibido no menu de comandos (Cmd+K)
shortLabelNãoRótulo mais curto exibido no botão fixado de ação rápida
iconNãoNome do ícone exibido ao lado do rótulo (por exemplo, 'IconBolt', 'IconSend')
isPinnedNãoQuando true, mostra o comando como um botão de ação rápida no canto superior direito da página
availabilityTypeNãoControla onde o comando aparece: 'GLOBAL' (sempre disponível), 'RECORD_SELECTION' (apenas quando registros estão selecionados) ou 'FALLBACK' (exibido quando nenhum outro comando corresponde)
availabilityObjectUniversalIdentifierNãoRestringe o comando a páginas de um tipo específico de objeto (por exemplo, somente em registros de Company)
conditionalAvailabilityExpressionNãoUma expressão booleana para controlar dinamicamente se o comando é visível (veja abaixo)

Expressões de disponibilidade condicional

O campo conditionalAvailabilityExpression permite controlar quando um comando é visível com base no contexto da página atual. Importe variáveis tipadas e operadores de twenty-sdk para construir expressões:
import {
  defineFrontComponent,
  pageType,
  numberOfSelectedRecords,
  objectPermissions,
  everyEquals,
  isDefined,
} from 'twenty-sdk';

export default defineFrontComponent({
  universalIdentifier: '...',
  name: 'bulk-action',
  component: BulkAction,
  command: {
    universalIdentifier: '...',
    label: 'Bulk Update',
    availabilityType: 'RECORD_SELECTION',
    conditionalAvailabilityExpression: everyEquals(
      objectPermissions,
      'canUpdateObjectRecords',
      true,
    ),
  },
});
Variáveis de contexto — representam o estado atual da página:
VariávelTipoDescrição
pageTypestringTipo de página atual (por exemplo, 'RecordIndexPage', 'RecordShowPage')
isInSidePanelbooleanSe o componente é renderizado em um painel lateral
numberOfSelectedRecordsnumberNúmero de registros atualmente selecionados
isSelectAllbooleanSe “selecionar tudo” está ativo
selectedRecordsarrayOs objetos de registro selecionados
favoriteRecordIdsarrayIDs dos registros marcados como favoritos
objectPermissionsobjectPermissões para o tipo de objeto atual
targetObjectReadPermissionsobjectPermissões de leitura para o objeto alvo
targetObjectWritePermissionsobjectPermissões de escrita para o objeto alvo
featureFlagsobjectFlags de recurso ativas
objectMetadataItemobjectMetadados do tipo de objeto atual
hasAnySoftDeleteFilterOnViewbooleanSe a visualização atual tem um filtro de soft-delete
Operadores — combine variáveis em expressões booleanas:
OperadorDescrição
isDefined(value)true se o valor não for null/undefined
isNonEmptyString(value)true se o valor for uma string não vazia
includes(array, value)true se o array contiver o valor
includesEvery(array, prop, value)true se a propriedade de cada item incluir o valor
every(array, prop)true se a propriedade for truthy em cada item
everyDefined(array, prop)true se a propriedade estiver definida em cada item
everyEquals(array, prop, value)true se a propriedade for igual ao valor em cada item
some(array, prop)true se a propriedade for truthy em pelo menos um item
someDefined(array, prop)true se a propriedade estiver definida em pelo menos um item
someEquals(array, prop, value)true se a propriedade for igual ao valor em pelo menos um item
someNonEmptyString(array, prop)true se a propriedade for uma string não vazia em pelo menos um item
none(array, prop)true se a propriedade for falsy em cada item
noneDefined(array, prop)true se a propriedade for undefined em cada item
noneEquals(array, prop, value)true se a propriedade não for igual ao valor em nenhum item

Recursos públicos

Componentes de front-end podem acessar arquivos do diretório public/ do app usando getPublicAssetUrl:
import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk';

const Logo = () => <img src={getPublicAssetUrl('logo.png')} alt="Logo" />;

export default defineFrontComponent({
  universalIdentifier: '...',
  name: 'logo',
  component: Logo,
});
Veja a seção de recursos públicos para obter detalhes.

Estilização

Componentes de front-end suportam várias abordagens de estilização. Você pode usar:
  • Estilos inlinestyle={{ color: 'red' }}
  • Componentes de UI do Twenty — importe de twenty-sdk/ui (Button, Tag, Status, Chip, Avatar e mais)
  • Emotion — CSS-in-JS com @emotion/react
  • Styled-components — padrões styled.div
  • Tailwind CSS — classes utilitárias
  • Qualquer biblioteca CSS-in-JS compatível com React
import { defineFrontComponent } from 'twenty-sdk';
import { Button, Tag, Status } from 'twenty-sdk/ui';

const StyledWidget = () => {
  return (
    <div style={{ padding: '16px', display: 'flex', gap: '8px' }}>
      <Button title="Click me" onClick={() => alert('Clicked!')} />
      <Tag text="Active" color="green" />
      <Status color="green" text="Online" />
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-567890123456',
  name: 'styled-widget',
  component: StyledWidget,
});
As habilidades definem instruções e capacidades reutilizáveis que os agentes de IA podem usar no seu espaço de trabalho. Use defineSkill() para definir habilidades com validação integrada:
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`,
});
Pontos-chave:
  • name é uma string de identificador exclusivo para a habilidade (recomenda-se kebab-case).
  • label é o nome de exibição legível por humanos mostrado na UI.
  • content contém as instruções da habilidade — este é o texto que o agente de IA usa.
  • icon (opcional) define o ícone exibido na UI.
  • description (opcional) fornece contexto adicional sobre a finalidade da habilidade.
Agentes são assistentes de IA que vivem dentro do seu espaço de trabalho. Use defineAgent() para criar agentes com um prompt de sistema personalizado:
src/agents/example-agent.ts
import { defineAgent } from 'twenty-sdk';

export default defineAgent({
  universalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
  name: 'sales-assistant',
  label: 'Sales Assistant',
  description: 'Helps the sales team draft outreach emails and research prospects',
  icon: 'IconRobot',
  prompt: 'You are a helpful sales assistant. Help users with their questions and tasks.',
});
Pontos-chave:
  • name é a string de identificador exclusiva do agente (recomenda-se kebab-case).
  • label é o nome de exibição mostrado na UI.
  • prompt é o prompt do sistema que define o comportamento do agente.
  • description (opcional) fornece contexto sobre o que o agente faz.
  • icon (opcional) define o ícone exibido na UI.
  • modelId (opcional) substitui o modelo de IA padrão usado pelo agente.
As visualizações são configurações salvas de como os registros de um objeto são exibidos — incluindo quais campos são visíveis, sua ordem e quaisquer filtros ou grupos aplicados. Use defineView() para enviar visualizações pré-configuradas com seu app:
src/views/example-view.ts
import { defineView, ViewKey } from 'twenty-sdk';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
import { NAME_FIELD_UNIVERSAL_IDENTIFIER } from '../objects/example-object';

export default defineView({
  universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
  name: 'All example items',
  objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
  icon: 'IconList',
  key: ViewKey.INDEX,
  position: 0,
  fields: [
    {
      universalIdentifier: 'f926bdb7-6af7-4683-9a09-adbca56c29f0',
      fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
      position: 0,
      isVisible: true,
      size: 200,
    },
  ],
});
Pontos-chave:
  • objectUniversalIdentifier especifica a qual objeto esta visualização se aplica.
  • key determina o tipo de visualização (por exemplo, ViewKey.INDEX para a visualização de lista principal).
  • fields controla quais colunas aparecem e sua ordem. Cada campo referencia um fieldMetadataUniversalIdentifier.
  • Você também pode definir filters, filterGroups, groups e fieldGroups para configurações mais avançadas.
  • position controla a ordenação quando existem várias visualizações para o mesmo objeto.
Os itens do menu de navegação adicionam entradas personalizadas à barra lateral do espaço de trabalho. Use defineNavigationMenuItem() para vincular a visualizações, URLs externas ou objetos:
src/navigation-menu-items/example-navigation-menu-item.ts
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk';
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from '../views/example-view';

export default defineNavigationMenuItem({
  universalIdentifier: '9327db91-afa1-41b6-bd9d-2b51a26efb4c',
  name: 'example-navigation-menu-item',
  icon: 'IconList',
  color: 'blue',
  position: 0,
  type: NavigationMenuItemType.VIEW,
  viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
});
Pontos-chave:
  • type determina para o que o item de menu aponta: NavigationMenuItemType.VIEW para uma visualização salva ou NavigationMenuItemType.LINK para uma URL externa.
  • Para links de visualização, defina viewUniversalIdentifier. Para links externos, defina link.
  • position controla a ordenação na barra lateral.
  • icon e color (opcionais) personalizam a aparência.
Layouts de página permitem personalizar como uma página de detalhes do registro se parece — quais abas aparecem, quais widgets estão dentro de cada aba e como eles são organizados. Use definePageLayout() para enviar layouts personalizados com seu app:
src/page-layouts/example-record-page-layout.ts
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';

export default definePageLayout({
  universalIdentifier: '203aeb94-6701-46d6-9af1-be2bbcc9e134',
  name: 'Example Record Page',
  type: 'RECORD_PAGE',
  objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
  tabs: [
    {
      universalIdentifier: '6ed26b60-a51d-4ad7-86dd-1c04c7f3cac5',
      title: 'Hello World',
      position: 50,
      icon: 'IconWorld',
      layoutMode: PageLayoutTabLayoutMode.CANVAS,
      widgets: [
        {
          universalIdentifier: 'aa4234e0-2e5f-4c02-a96a-573449e2351d',
          title: 'Hello World',
          type: 'FRONT_COMPONENT',
          configuration: {
            configurationType: 'FRONT_COMPONENT',
            frontComponentUniversalIdentifier:
              HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
          },
        },
      ],
    },
  ],
});
Pontos-chave:
  • type geralmente é 'RECORD_PAGE' para personalizar a visualização de detalhes de um objeto específico.
  • objectUniversalIdentifier especifica a qual objeto este layout se aplica.
  • Cada tab define uma seção da página com um title, position e layoutMode (CANVAS para layout livre).
  • Cada widget dentro de uma aba pode renderizar um componente de front-end, uma lista de relações ou outros tipos de widget incorporados.
  • position nas abas controla sua ordem. Use valores mais altos (por exemplo, 50) para colocar abas personalizadas após as nativas.

Recursos públicos (pasta public/)

A pasta public/ na raiz do seu app contém arquivos estáticos — imagens, ícones, fontes ou quaisquer outros recursos de que seu app precisa em tempo de execução. Esses arquivos são incluídos automaticamente nas compilações, sincronizados durante o modo de desenvolvimento e enviados para o servidor. Arquivos colocados em public/ são:
  • Publicamente acessíveis — depois de sincronizados com o servidor, os recursos são servidos em uma URL pública. Não é necessária autenticação para acessá-los.
  • Disponíveis em componentes de front-end — use URLs de recursos para exibir imagens, ícones ou qualquer mídia dentro de seus componentes React.
  • Disponíveis em funções lógicas — referencie URLs de recursos em e-mails, respostas de API ou qualquer lógica no lado do servidor.
  • Usados para metadados do marketplace — os campos logoUrl e screenshots em defineApplication() referenciam arquivos desta pasta (por exemplo, public/logo.png). Eles são exibidos no marketplace quando seu app é publicado.
  • Sincronizados automaticamente no modo de desenvolvimento — quando você adiciona, atualiza ou exclui um arquivo em public/, ele é sincronizado automaticamente com o servidor. Não é necessário reiniciar.
  • Incluídos nas compilaçõesyarn twenty build agrupa todos os recursos públicos na saída de distribuição.

Acessando recursos públicos com getPublicAssetUrl

Use o helper getPublicAssetUrl de twenty-sdk para obter a URL completa de um arquivo no seu diretório public/. Funciona tanto em funções lógicas quanto em componentes de front-end. Em uma função lógica:
src/logic-functions/send-invoice.ts
import { defineLogicFunction, getPublicAssetUrl } from 'twenty-sdk';

const handler = async (): Promise<any> => {
  const logoUrl = getPublicAssetUrl('logo.png');
  const invoiceUrl = getPublicAssetUrl('templates/invoice.png');

  // Fetch the file content (no auth required — public endpoint)
  const response = await fetch(invoiceUrl);
  const buffer = await response.arrayBuffer();

  return { logoUrl, size: buffer.byteLength };
};

export default defineLogicFunction({
  universalIdentifier: 'a1b2c3d4-...',
  name: 'send-invoice',
  description: 'Sends an invoice with the app logo',
  timeoutSeconds: 10,
  handler,
});
Em um componente de front-end:
src/front-components/company-card.tsx
import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk';

export default defineFrontComponent(() => {
  const logoUrl = getPublicAssetUrl('logo.png');

  return <img src={logoUrl} alt="App logo" />;
});
O argumento path é relativo à pasta public/ do seu app. Tanto getPublicAssetUrl('logo.png') quanto getPublicAssetUrl('public/logo.png') resolvem para a mesma URL — o prefixo public/ é removido automaticamente, se presente.

Usando pacotes npm

Você pode instalar e usar qualquer pacote npm no seu app. Tanto funções lógicas quanto componentes de front-end são empacotados com esbuild, que incorpora todas as dependências na saída — nenhum node_modules é necessário em tempo de execução.

Instalando um pacote

yarn add axios
Em seguida, importe-o no seu código:
src/logic-functions/fetch-data.ts
import { defineLogicFunction } from 'twenty-sdk';
import axios from 'axios';

const handler = async (): Promise<any> => {
  const { data } = await axios.get('https://api.example.com/data');

  return { data };
};

export default defineLogicFunction({
  universalIdentifier: '...',
  name: 'fetch-data',
  description: 'Fetches data from an external API',
  timeoutSeconds: 10,
  handler,
});
O mesmo vale para componentes de front-end:
src/front-components/chart.tsx
import { defineFrontComponent } from 'twenty-sdk';
import { format } from 'date-fns';

const DateWidget = () => {
  return <p>Today is {format(new Date(), 'MMMM do, yyyy')}</p>;
};

export default defineFrontComponent({
  universalIdentifier: '...',
  name: 'date-widget',
  component: DateWidget,
});

Como o empacotamento funciona

A etapa de build (yarn twenty dev ou yarn twenty build) usa o esbuild para produzir um único arquivo independente por função lógica e por componente de front-end. Todos os pacotes importados são incorporados ao bundle. Funções lógicas são executadas em um ambiente Node.js. Módulos nativos do Node (fs, path, crypto, http, etc.) estão disponíveis e não precisam ser instalados. Componentes de front-end são executados em um Web Worker. Módulos nativos do Node não estão disponíveis — apenas APIs do navegador e pacotes npm que funcionam em um ambiente de navegador. Ambos os ambientes têm twenty-client-sdk/core e twenty-client-sdk/metadata disponíveis como módulos pré-fornecidos — eles não são empacotados, mas resolvidos em tempo de execução pelo servidor.

Gerando entidades com yarn twenty add

Em vez de criar arquivos de entidade manualmente, você pode usar o scaffolder interativo:
yarn twenty add
Isso solicita que você escolha um tipo de entidade e orienta você pelos campos obrigatórios. Ele gera um arquivo pronto para uso com um universalIdentifier estável e a chamada correta de defineEntity(). Você também pode passar o tipo de entidade diretamente para pular o primeiro prompt:
yarn twenty add object
yarn twenty add logicFunction
yarn twenty add frontComponent

Tipos de entidade disponíveis

Tipo de entidadeComandoArquivo gerado
Objetoyarn twenty add objectsrc/objects/<name>.ts
Campoyarn twenty add fieldsrc/fields/<name>.ts
Função lógicayarn twenty add logicFunctionsrc/logic-functions/<name>.ts
Componente de front-endyarn twenty add frontComponentsrc/front-components/<name>.tsx
Funçãoyarn twenty add rolesrc/roles/<name>.ts
Habilidadeyarn twenty add skillsrc/skills/<name>.ts
Agenteyarn twenty add agentsrc/agents/<name>.ts
Vistayarn twenty add viewsrc/views/<name>.ts
Item do menu de navegaçãoyarn twenty add navigationMenuItemsrc/navigation-menu-items/<name>.ts
Layout da páginayarn twenty add pageLayoutsrc/page-layouts/<name>.ts

O que o scaffolder gera

Cada tipo de entidade tem seu próprio modelo. Por exemplo, yarn twenty add object solicita:
  1. Nome (singular) — por exemplo, invoice
  2. Nome (plural) — por exemplo, invoices
  3. Rótulo (singular) — preenchido automaticamente a partir do nome (por exemplo, Invoice)
  4. Rótulo (plural) — preenchido automaticamente (por exemplo, Invoices)
  5. Criar uma view e um item de navegação? — se você responder sim, o scaffolder também gera uma view correspondente e um link na barra lateral para o novo objeto.
Outros tipos de entidade têm prompts mais simples — a maioria pede apenas um nome. O tipo de entidade field é mais detalhado: ele solicita o nome do campo, rótulo, tipo (a partir de uma lista de todos os tipos de campo disponíveis como TEXT, NUMBER, SELECT, RELATION, etc.) e o universalIdentifier do objeto de destino.

Caminho de saída personalizado

Use a opção --path para colocar o arquivo gerado em um local personalizado:
yarn twenty add logicFunction --path src/custom-folder

Clientes de API tipados (twenty-client-sdk)

O pacote twenty-client-sdk fornece dois clientes GraphQL tipados para interagir com a API do Twenty a partir das suas funções de lógica e componentes de front-end.
ClienteImportarEndpointGerado?
CoreApiClienttwenty-client-sdk/core/graphql — dados do espaço de trabalho (registros, objetos)Sim, em tempo de dev/build
MetadataApiClienttwenty-client-sdk/metadata/metadata — configuração do espaço de trabalho, upload de arquivosNão, vem pré-compilado
CoreApiClient é o cliente principal para consultar e mutar dados do espaço de trabalho. Ele é gerado a partir do schema do seu espaço de trabalho durante yarn twenty dev ou yarn twenty build, então é totalmente tipado para corresponder aos seus objetos e campos.
import { CoreApiClient } from 'twenty-client-sdk/core';

const client = new CoreApiClient();

// Query records
const { companies } = await client.query({
  companies: {
    edges: {
      node: {
        id: true,
        name: true,
        domainName: {
          primaryLinkLabel: true,
          primaryLinkUrl: true,
        },
      },
    },
  },
});

// Create a record
const { createCompany } = await client.mutation({
  createCompany: {
    __args: {
      data: {
        name: 'Acme Corp',
      },
    },
    id: true,
    name: true,
  },
});
O cliente usa uma sintaxe de selection-set: passe true para incluir um campo, use __args para argumentos e aninhe objetos para relações. Você tem preenchimento automático e verificação de tipos completos com base no schema do seu espaço de trabalho.
CoreApiClient é gerado em tempo de dev/build. Se você usá-lo sem executar primeiro yarn twenty dev ou yarn twenty build, ele lançará um erro. A geração ocorre automaticamente — a CLI analisa o schema GraphQL do seu espaço de trabalho e gera um cliente tipado usando @genql/cli.

Usando CoreSchema para anotações de tipo

CoreSchema fornece tipos TypeScript que correspondem aos objetos do seu espaço de trabalho — útil para tipar o estado de componentes ou parâmetros de função:
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
import { useState } from 'react';

const [company, setCompany] = useState<
  Pick<CoreSchema.Company, 'id' | 'name'> | undefined
>(undefined);

const client = new CoreApiClient();
const result = await client.query({
  company: {
    __args: { filter: { position: { eq: 1 } } },
    id: true,
    name: true,
  },
});
setCompany(result.company);
MetadataApiClient é fornecido pré-compilado com o SDK (não é necessário gerar). Ele consulta o endpoint /metadata para configuração do espaço de trabalho, aplicativos e upload de arquivos.
import { MetadataApiClient } from 'twenty-client-sdk/metadata';

const metadataClient = new MetadataApiClient();

// List first 10 objects in the workspace
const { objects } = await metadataClient.query({
  objects: {
    edges: {
      node: {
        id: true,
        nameSingular: true,
        namePlural: true,
        labelSingular: true,
        isCustom: true,
      },
    },
    __args: {
      filter: {},
      paging: { first: 10 },
    },
  },
});

Carregamento de arquivos

MetadataApiClient inclui um método uploadFile para anexar arquivos a campos do tipo arquivo:
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
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
  '58a0a314-d7ea-4865-9850-7fb84e72f30b',            // field universalIdentifier
);

console.log(uploadedFile);
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
ParâmetroTipoDescrição
fileBufferBufferO conteúdo bruto do arquivo
filenamestringO nome do arquivo (usado para armazenamento e exibição)
contentTypestringTipo MIME (padrão para application/octet-stream se omitido)
fieldMetadataUniversalIdentifierstringO universalIdentifier do campo do tipo arquivo no seu objeto
Pontos-chave:
  • Usa o universalIdentifier do campo (não o ID específico do espaço de trabalho), de modo que seu código de upload funcione em qualquer espaço de trabalho onde seu app esteja instalado.
  • A url retornada é um URL assinado que você pode usar para acessar o arquivo enviado.
Quando seu código é executado no Twenty (funções de lógica ou componentes de front-end), a plataforma injeta credenciais como variáveis de ambiente:
  • TWENTY_API_URL — URL base da API do Twenty
  • TWENTY_APP_ACCESS_TOKEN — Chave de curta duração com escopo para o papel de função padrão do seu aplicativo
Você não precisa passá-las para os clientes — eles leem de process.env automaticamente. As permissões da chave de API são determinadas pelo papel referenciado em defaultRoleUniversalIdentifier no seu application-config.ts.

Testando seu aplicativo

O SDK fornece APIs programáticas que permitem compilar, implantar, instalar e desinstalar seu aplicativo a partir de código de teste. Em conjunto com Vitest e os clientes de API tipados, você pode escrever testes de integração que verificam que seu aplicativo funciona de ponta a ponta em um servidor Twenty real.

Configuração

O aplicativo gerado pelo scaffolder já inclui o Vitest. Se você configurá-lo manualmente, instale as dependências:
yarn add -D vitest vite-tsconfig-paths
Crie um vitest.config.ts na raiz do seu aplicativo:
vitest.config.ts
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';

export default defineConfig({
  plugins: [
    tsconfigPaths({
      projects: ['tsconfig.spec.json'],
      ignoreConfigErrors: true,
    }),
  ],
  test: {
    testTimeout: 120_000,
    hookTimeout: 120_000,
    include: ['src/**/*.integration-test.ts'],
    setupFiles: ['src/__tests__/setup-test.ts'],
    env: {
      TWENTY_API_URL: 'http://localhost:2020',
      TWENTY_API_KEY: 'your-api-key',
    },
  },
});
Crie um arquivo de configuração que verifique se o servidor está acessível antes da execução dos testes:
src/__tests__/setup-test.ts
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { beforeAll } from 'vitest';

const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');

beforeAll(async () => {
  // Verify the server is running
  const response = await fetch(`${TWENTY_API_URL}/healthz`);

  if (!response.ok) {
    throw new Error(
      `Twenty server is not reachable at ${TWENTY_API_URL}. ` +
        'Start the server before running integration tests.',
    );
  }

  // Write a temporary config for the SDK
  fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });

  fs.writeFileSync(
    path.join(TEST_CONFIG_DIR, 'config.json'),
    JSON.stringify({
      remotes: {
        local: {
          apiUrl: process.env.TWENTY_API_URL,
          apiKey: process.env.TWENTY_API_KEY,
        },
      },
      defaultRemote: 'local',
    }, null, 2),
  );
});

APIs programáticas do SDK

O subcaminho twenty-sdk/cli exporta funções que você pode chamar diretamente a partir do código de teste:
FunçãoDescrição
appBuildCompilar o aplicativo e, opcionalmente, empacotar um tarball
appDeployEnviar um tarball para o servidor
appInstallInstalar o aplicativo no espaço de trabalho ativo
appUninstallDesinstalar o aplicativo do espaço de trabalho ativo
Cada função retorna um objeto de resultado com success: boolean e data ou error.

Escrevendo um teste de integração

Aqui está um exemplo completo que compila, implanta e instala o aplicativo e, em seguida, verifica se ele aparece no espaço de trabalho:
src/__tests__/app-install.integration-test.ts
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

const APP_PATH = process.cwd();

describe('App installation', () => {
  beforeAll(async () => {
    const buildResult = await appBuild({
      appPath: APP_PATH,
      tarball: true,
      onProgress: (message: string) => console.log(`[build] ${message}`),
    });

    if (!buildResult.success) {
      throw new Error(`Build failed: ${buildResult.error?.message}`);
    }

    const deployResult = await appDeploy({
      tarballPath: buildResult.data.tarballPath!,
      onProgress: (message: string) => console.log(`[deploy] ${message}`),
    });

    if (!deployResult.success) {
      throw new Error(`Deploy failed: ${deployResult.error?.message}`);
    }

    const installResult = await appInstall({ appPath: APP_PATH });

    if (!installResult.success) {
      throw new Error(`Install failed: ${installResult.error?.message}`);
    }
  });

  afterAll(async () => {
    await appUninstall({ appPath: APP_PATH });
  });

  it('should find the installed app in the workspace', async () => {
    const metadataClient = new MetadataApiClient();

    const result = await metadataClient.query({
      findManyApplications: {
        id: true,
        name: true,
        universalIdentifier: true,
      },
    });

    const installedApp = result.findManyApplications.find(
      (app: { universalIdentifier: string }) =>
        app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
    );

    expect(installedApp).toBeDefined();
  });
});

Executando testes

Certifique-se de que seu servidor Twenty local esteja em execução e, em seguida:
yarn test
Ou no modo watch durante o desenvolvimento:
yarn test:watch

Verificação de tipos

Você também pode executar a verificação de tipos no seu aplicativo sem executar os testes:
yarn twenty typecheck
Isso executa tsc --noEmit e informa quaisquer erros de tipo.

Referência da CLI

Além de dev, build, add e typecheck, a CLI fornece comandos para executar funções, visualizar logs e gerenciar instalações de aplicativos.

Executando funções (yarn twenty exec)

Execute manualmente uma função de lógica sem acioná-la via HTTP, cron ou evento de banco de dados:
# Execute by function name
yarn twenty exec -n create-new-post-card

# Execute by universalIdentifier
yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf

# Pass a JSON payload
yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}'

# Execute pre-install or post-install functions
yarn twenty exec --preInstall
yarn twenty exec --postInstall

Visualizando logs de funções (yarn twenty logs)

Transmita os logs de execução das funções de lógica do seu aplicativo:
# Stream all function logs
yarn twenty logs

# Filter by function name
yarn twenty logs -n create-new-post-card

# Filter by universalIdentifier
yarn twenty logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
Isso é diferente de yarn twenty server logs, que mostra os logs do contêiner Docker. yarn twenty logs mostra os logs de execução de funções do seu aplicativo a partir do servidor Twenty.

Desinstalando um aplicativo (yarn twenty uninstall)

Remova seu aplicativo do espaço de trabalho ativo:
yarn twenty uninstall

# Skip the confirmation prompt
yarn twenty uninstall --yes

Gerenciando remotos

Um remoto é um servidor Twenty ao qual seu aplicativo se conecta. Durante a configuração, o gerador de scaffold cria um para você automaticamente. Você pode adicionar mais remotos ou alternar entre eles a qualquer momento.
# Add a new remote (opens a browser for OAuth login)
yarn twenty remote add

# Connect to a local Twenty server (auto-detects port 2020 or 3000)
yarn twenty remote add --local

# Add a remote non-interactively (useful for CI)
yarn twenty remote add --api-url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote

# List all configured remotes
yarn twenty remote list

# Switch the active remote
yarn twenty remote switch <name>
Suas credenciais são armazenadas em ~/.twenty/config.json.

CI com GitHub Actions

O gerador de scaffold cria um workflow do GitHub Actions pronto para uso em .github/workflows/ci.yml. Ele executa seus testes de integração automaticamente a cada push para main e em pull requests. O workflow:
  1. Faz checkout do seu código
  2. Inicializa um servidor Twenty temporário usando a ação twentyhq/twenty/.github/actions/spawn-twenty-docker-image
  3. Instala as dependências com yarn install --immutable
  4. Executa yarn test com TWENTY_API_URL e TWENTY_API_KEY injetados a partir das saídas da ação
.github/workflows/ci.yml
name: CI

on:
  push:
    branches:
      - main
  pull_request: {}

env:
  TWENTY_VERSION: latest

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Spawn Twenty instance
        id: twenty
        uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main
        with:
          twenty-version: ${{ env.TWENTY_VERSION }}
          github-token: ${{ secrets.GITHUB_TOKEN }}

      - name: Enable Corepack
        run: corepack enable

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
          cache: 'yarn'

      - name: Install dependencies
        run: yarn install --immutable

      - name: Run integration tests
        run: yarn test
        env:
          TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
          TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }}
Você não precisa configurar nenhum segredo — a ação spawn-twenty-docker-image inicia um servidor Twenty efêmero diretamente no runner e fornece os detalhes de conexão. O segredo GITHUB_TOKEN é fornecido automaticamente pelo GitHub. Para fixar uma versão específica do Twenty em vez de latest, altere a variável de ambiente TWENTY_VERSION no topo do workflow.