snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
---
title: APIs
icon: plug
description: APIs REST e GraphQL geradas a partir do esquema do seu espaço de trabalho.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
## APIs com esquema por locatário
Não há uma referência de API estática para o Twenty. Cada espaço de trabalho tem seu próprio esquema — quando você adiciona um objeto personalizado (por exemplo, `Invoice`), ele imediatamente recebe endpoints REST e GraphQL idênticos aos dos objetos nativos, como `Company` ou `Person`. A API é gerada a partir do esquema, então os endpoints usam diretamente os nomes dos seus objetos e campos — sem IDs opacos.
Sua documentação de API específica do seu espaço de trabalho fica disponível em **Configurações → API & Webhooks** após criar uma chave de API. Ela inclui um playground interativo onde você pode executar chamadas reais contra seus dados.
## Duas APIs
**API principal** — `/rest/` e `/graphql/`
CRUD em registros: Pessoas, Empresas, Oportunidades, seus objetos personalizados. Consultar, filtrar, percorrer relações.
**API de Metadados** — `/rest/metadata/` e `/metadata/`
Gerenciamento de esquema: criar/modificar/excluir objetos, campos e relações. É assim que você altera programaticamente seu modelo de dados.
Ambas estão disponíveis como REST e GraphQL. GraphQL adiciona upserts em lote e a capacidade de percorrer relações em uma única consulta. Os mesmos dados subjacentes em ambos os casos.
## URLs base
| Ambiente | URL base |
| -------------- | ------------------------- |
| Nuvem | `https://api.twenty.com/` |
| Auto-hospedado | `https://{your-domain}/` |
## Autenticação
```
Authorization: Bearer YOUR_API_KEY
```
Crie uma chave de API em **Settings → API & Webhooks → + Create key**. Copie-a imediatamente — ela é exibida apenas uma vez. As chaves podem ser limitadas a uma função específica em **Settings → Members → Roles → Assignment tab** para restringir o que podem acessar.
<VimeoEmbed videoId="928786722" title="Criando chave de API" />
Para acesso baseado em OAuth (aplicativos externos atuando em nome dos usuários), veja [OAuth](/l/pt/developers/extend/oauth).
## Operações em lote
Tanto REST quanto GraphQL suportam processamento em lote de até 60 registros por solicitação — criar, atualizar ou excluir. O GraphQL também oferece suporte a upsert em lote (criar ou atualizar em uma única chamada) usando nomes no plural como `CreateCompanies`.
## Limites de taxa
| Limite | Valor |
| --------------- | ------------------------ |
| Solicitações | 100 por minuto |
| Tamanho do lote | 60 registros por chamada |

View File

@@ -0,0 +1,124 @@
---
title: Configuração da aplicação
description: Declare a identidade do seu app, o papel padrão, as variáveis e os metadados de marketplace com `defineApplication`.
icon: rocket
---
Todo app deve ter exatamente uma chamada a `defineApplication`. Ela declara:
* **Identidade** — identificador universal, nome de exibição, descrição.
* **Permissões** — qual papel é usado pelas suas funções de lógica e pelos componentes de front-end.
* **Variáveis** *(opcional)* — pares chave–valor expostos ao seu código como variáveis de ambiente.
* **Hooks de pré-instalação/pós-instalação/desinstalação** *(opcional)* — consulte [Funções de lógica](/l/pt/developers/extend/apps/logic/logic-functions).
```ts src/application-config.ts
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
displayName: 'My Twenty App',
description: 'My first Twenty app',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
description: 'Default recipient name for postcards',
value: 'Jane Doe',
isSecret: false,
},
},
});
```
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. Em funções lógicas (no lado do servidor), elas ficam disponíveis como `process.env.VARIABLE_NAME`. Em componentes de front-end, use `getApplicationVariable('VARIABLE_NAME')` de `twenty-sdk/front-component`. Variáveis marcadas com `isSecret: true` são injetadas apenas em funções lógicas. Componentes de front-end recebem apenas variáveis não secretas.
* O papel padrão é detectado automaticamente a partir do arquivo de definição de papel marcado com [`defineApplicationRole()`](/l/pt/developers/extend/apps/config/roles) — você não precisa referenciá-lo em `defineApplication()`.
* As funções de pré-instalação, pós-instalação e desinstalação são detectadas automaticamente durante a construção do manifesto — você não precisa referenciá-las em `defineApplication()`.
* Passar `defaultRoleUniversalIdentifier` explicitamente ainda é compatível para retrocompatibilidade, mas foi preterido em favor de `defineApplicationRole()`.
* `serverVariables` são configurações e segredos com escopo de instância (por exemplo, chaves de API). Ao contrário de `applicationVariables`, eles não declaram nenhum valor no manifesto — o operador do workspace os preenche nas configurações do app, e eles são injetados nas funções de lógica somente depois de definidos.
* Ambos os tipos de variável aceitam `isDeprecated: true`. Use isso para aposentar uma variável em vez de excluí-la: manter a chave declarada preserva o valor armazenado (excluí-la destrói o valor inserido pelo operador), e a variável ainda é injetada, para que seu código possa fazer fallback para ela — `process.env.NEW_API_KEY ?? process.env.API_KEY`. Uma variável obsoleta desaparece das configurações do aplicativo assim que não tiver valor e nunca é contabilizada na verificação de configuração do aplicativo, portanto `isDeprecated` prevalece sobre `isRequired`.
* Para renderizar uma interface de configuração personalizada na guia **Configurações** do app (no lugar da seção padrão de configuração de variáveis), declare um componente de front-end com [`defineSettingsFrontComponent()`](/l/pt/developers/extend/apps/layout/front-components#custom-settings-component) em seu próprio arquivo. Só é permitido um por app. Seções gerenciadas pelo sistema (atualização automática, URL do app, conexões) permanecem sempre visíveis.
## Tipos de variáveis
Tanto `applicationVariables` quanto `serverVariables` aceitam um `type` opcional (e, para `SELECT` / `MULTI_SELECT`, uma lista de `options`). Tipos compatíveis: `TEXT` (padrão), `BOOLEAN`, `NUMBER`, `NUMERIC`, `DATE`, `DATE_TIME`, `SELECT`, `MULTI_SELECT`, `ARRAY`, `RAW_JSON`, `RICH_TEXT`.
```ts src/application-config.ts
import { defineApplication, FieldType } from 'twenty-sdk/define';
export default defineApplication({
// ...identity, role...
applicationVariables: {
MAX_POSTCARDS: {
universalIdentifier: '5f4497e4-9030-4085-85eb-2c48b8d53713',
description: 'Maximum postcards per batch',
type: FieldType.NUMBER,
value: 10,
},
DEFAULT_REGION: {
universalIdentifier: '76c5c321-b6b6-46eb-b4fc-f9f04bb04227',
description: 'Default shipping region',
type: FieldType.SELECT,
options: [
{ label: 'Europe', value: 'eu' },
{ label: 'United States', value: 'us' },
],
value: 'eu',
},
},
});
```
O `type` afeta apenas a **apresentação e validação** — ele seleciona a entrada correspondente na interface de configurações do workspace (um toggle, campo numérico, dropdown, seletor de data, editor JSON, …) e permite que o build valide a sua configuração (por exemplo, `SELECT` / `MULTI_SELECT` devem declarar `options` não vazias). Ele **não** altera a forma como o valor chega ao seu código.
Os valores são **sempre injetados como strings** — isso é inerente às variáveis de ambiente (`process.env.*` aceita apenas string). Quando a sua função de lógica é executada, o executor serializa cada valor de acordo com o `type` declarado ao construir o `process.env`, para que o formato da string seja consistente, não importa como o valor foi definido (padrão do manifesto, interface de configurações ou uma versão anterior):
| Tipo | string de `process.env` |
| ------------------------------------- | -------------------------------------- |
| `TEXT`, `SELECT`, `DATE`, `DATE_TIME` | o valor bruto (`"eu"`, `"2026-01-01"`) |
| `BOOLEAN` | `"true"` / `"false"` |
| `NUMBER`, `NUMERIC` | string decimal (`"10"`, `"2.5"`) |
| `MULTI_SELECT`, `ARRAY` | array JSON (`'["email","postcard"]'`) |
| `RAW_JSON`, `RICH_TEXT` | objeto JSON (`'{"retries":3}'`) |
Converta a string de volta para o tipo que você espera:
```ts
const maxCards = Number(process.env.MAX_POSTCARDS); // "10" -> 10
const enabled = process.env.ENABLE_TRACKING === 'true'; // "true" -> true
const channels = JSON.parse(process.env.ENABLED_CHANNELS ?? '[]'); // '["email"]' -> ["email"]
const config = JSON.parse(process.env.PROVIDER_CONFIG ?? '{}'); // '{"retries":3}' -> { retries: 3 }
```
O mesmo se aplica a componentes de front-end que leem valores via `getApplicationVariable('VARIABLE_NAME')` — o valor retornado é uma string; converta conforme necessário.
## Papel de função padrão
O papel declarado com [`defineApplicationRole()`](/l/pt/developers/extend/apps/config/roles) controla o que as funções de lógica e os componentes de front-end do aplicativo podem acessar:
* Os tokens de tempo de execução injetados nas suas funções de lógica são derivados desse papel. Uma chamada que atua como uma pessoa é ainda mais limitada pelo que essa pessoa pode fazer, portanto nunca pode exceder nenhum dos dois. Consulte [Qual acesso uma chamada usa](/l/pt/developers/extend/apps/logic/logic-functions#whose-access-a-call-uses).
* O cliente de API tipado é restrito às permissões concedidas a esse papel.
* Siga o princípio do menor privilégio: declare apenas as permissões de que suas funções precisam.
Ao criar um novo app com o scaffold, a CLI cria um arquivo de papel inicial em `src/roles/default-role.ts`. Consulte [Papéis e permissões](/l/pt/developers/extend/apps/config/roles) para a referência completa.
## Metadados do Marketplace
Se você planeja [publicar seu app](/l/pt/developers/extend/apps/operations/publishing), estes campos opcionais controlam como seu app aparece no marketplace:
| Campo | Descrição |
| ------------------ | ----------------------------------------------------------------------------------------------------------------- |
| `author` | Nome do autor ou da empresa |
| `category` | Categoria do app para filtragem no marketplace |
| `logo` | Caminho para o logotipo do seu aplicativo empacotado em `public/` (p. ex., `public/logo.png`) |
| `galleryImages` | Array de caminhos de imagem da galeria empacotado em `public/` (por exemplo, `public/screenshot-1.png`) |
| `aboutDescription` | Descrição em markdown mais longa para a aba "Sobre". Se omitido, o marketplace usa o `README.md` do pacote no npm |
| `websiteUrl` | Link para seu site |
| `termsUrl` | Link para os Termos de Serviço |
| `emailSupport` | Endereço de e-mail de suporte |
| `issueReportUrl` | Link para o rastreador de problemas |
<Note>
`logoUrl` e `screenshots` são aliases depreciadas de `logo` e `galleryImages`. URLs absolutas externas (`http://` ou `https://`) não são suportados para estes campos: eles são descartados com um aviso no momento da compilação. Agrupe as imagens na pasta `public/` do seu aplicativo.
</Note>

View File

@@ -0,0 +1,188 @@
---
title: Hooks de instalação
description: Execute lógica durante o ciclo de vida de instalação, atualização ou desinstalação — popule dados iniciais, faça backup de registros, valide a atualização, limpe recursos externos.
icon: wrench
---
Hooks de instalação são funções de lógica especiais que são executadas durante o ciclo de vida de instalação, atualização ou desinstalação. Elas compartilham o mesmo runtime de handler que as [logic functions](/l/pt/developers/extend/apps/logic/logic-functions) normais, mas são declaradas com suas próprias funções de definição e ficam fora do modelo de gatilhos normal (HTTP, cron, eventos de banco de dados). Hooks de instalação recebem um `InstallPayload` (`{ previousVersion?: string; newVersion: string }` — `previousVersion` é `undefined` em uma instalação nova); o hook de desinstalação recebe um `UninstallPayload` (`{ version?: string }` — a versão que está sendo removida).
Cada app pode definir **no máximo um** de cada hook (pre-instalação, pós-instalação, desinstalação). A geração do manifesto apresentará erro se mais de um de qualquer tipo for detectado.
```
┌─────────────────────────────────────────────────────────────┐
│ install flow │
│ │
│ upload package → [pre-install] → metadata migration → │
│ generate SDK → [post-install] │
│ │
│ old schema visible new schema visible │
└─────────────────────────────────────────────────────────────┘
```
## Visão geral
| | `definePreInstallLogicFunction` | `definePostInstallLogicFunction` |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| Execuções | Antes da migração de metadados — o esquema e os dados **anteriores** ainda estão intactos | Após a migração e a geração do SDK — o **novo** esquema está em vigor |
| Execução | Sempre síncrona; bloqueia a instalação | Assíncrona por padrão (em fila, 3 novas tentativas); modo síncrono por opt-in via `shouldRunSynchronously: true` |
| Em caso de falha | A instalação é **abortada** antes de qualquer alteração de esquema | Assíncrono: novas tentativas até 3 vezes. Síncrono: o chamador recebe `POST_INSTALL_ERROR` (as alterações de esquema **não** são revertidas) |
| Uso típico | Fazer backup ou corrigir dados que uma migração poderia perder; recusar uma atualização arriscada lançando uma exceção | Popular dados padrão, configurar o workspace, registrar recursos externos |
**Regra geral:** use post-install como padrão. Recurra à pré-instalação somente quando a própria migração for destrutiva e você precisar interceptar o estado anterior antes que ele desapareça.
| Você quer... | Usar |
| ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| Popular dados, configurar o workspace, registrar recursos externos | `post-install` |
| Trabalho de longa duração que não deve bloquear a resposta da instalação | `post-install` (modo assíncrono padrão, com novas tentativas do worker) |
| Configuração rápida da qual o chamador depende imediatamente após o retorno da instalação | `post-install` com `shouldRunSynchronously: true` |
| Ler ou fazer backup de dados que a próxima migração perderia | `pre-install` |
| Rejeitar uma atualização que corromperia dados existentes | `pre-install` (lançar uma exceção no manipulador) |
| Reconciliação em cada atualização | Qualquer um dos hooks com `shouldRunOnVersionUpgrade: true` |
## Comportamento compartilhado por ambos os hooks
* A configuração é uma config de `defineLogicFunction` menos as configurações de gatilho, mais `shouldRunOnVersionUpgrade`.
* **Quando é executado**: apenas em instalações novas, por padrão. Defina `shouldRunOnVersionUpgrade: true` para também executar em atualizações. Use `previousVersion` / `newVersion` para ramificar com base no caminho de atualização.
* **Idempotência é importante**: o post-install assíncrono pode ser executado novamente, e qualquer um dos hooks é reexecutado em atualizações quando `shouldRunOnVersionUpgrade` está ativado.
* O ambiente usual de logic-function (`APPLICATION_ID`, `APP_ACCESS_TOKEN`, `API_URL`) é injetado, para que você possa chamar a Twenty API com o token do seu app.
* O hook é anexado automaticamente ao manifesto da aplicação em tempo de build (`preInstallLogicFunction` / `postInstallLogicFunction`) — nada para referenciar em [`defineApplication()`](/l/pt/developers/extend/apps/config/application).
* O `timeoutSeconds` padrão é 300 para permitir tarefas de configuração mais longas, como o pré-carregamento de dados.
* **Não é executado em modo de desenvolvimento**: `yarn twenty dev` ignora o fluxo de instalação e sincroniza os arquivos diretamente, portanto os hooks nunca são executados ali. Em vez disso, acione-os manualmente:
```bash filename="Terminal"
yarn twenty dev:function:exec --postInstall
yarn twenty dev:function:exec --preInstall
```
<AccordionGroup>
<Accordion title="definePostInstallLogicFunction" description="É executada depois que a migração de metadados do workspace é aplicada">
É executado depois que seu app termina de ser instalado: metadados sincronizados, cliente SDK gerado, novo esquema disponível para consulta. Exemplo — popular um registro padrão em instalações novas:
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
if (previousVersion) return; // fresh installs only
const client = new CoreApiClient();
await client.mutation({
createPostCard: {
__args: { data: { name: 'Welcome to Postcard', content: 'Your first card!' } },
id: true,
},
});
};
export default definePostInstallLogicFunction({
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
name: 'post-install',
description: 'Seeds a welcome post card after install.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
shouldRunSynchronously: false,
handler,
});
```
A flag `shouldRunSynchronously` controla o modelo de execução:
* `false` *(padrão)* — colocado em fila na message queue (`retryLimit: 3`) e executado por um worker. A resposta da instalação retorna assim que o job é colocado na fila. **Use para trabalhos de longa duração** — popular grandes conjuntos de dados, APIs lentas de terceiros.
* `true` — executado inline durante o fluxo de instalação. A requisição de instalação fica bloqueada até que o handler termine; um erro lançado aparece como `POST_INSTALL_ERROR` para o chamador (sem novas tentativas). **Use para trabalhos rápidos que precisam ser concluídos antes da resposta.** A migração já foi aplicada neste ponto, portanto uma falha não reverte as alterações de esquema — ela apenas expõe o erro.
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="É executada antes que a migração de metadados do workspace seja aplicada">
É executado antes da migração de metadados, contra o esquema **anterior** — o lugar certo para fazer backup de dados que uma migração poderia perder ou para recusar uma atualização arriscada. Antes de executar, o servidor realiza uma "sincronização simplificada" puramente aditiva que registra apenas a função de pré-instalação da nova versão; todo o resto — objetos, campos e dados da versão anterior — permanece intocado quando seu handler é executado.
A pré-instalação é sempre **síncrona** e bloqueia a instalação. Se o handler lançar uma exceção, a instalação é abortada antes de qualquer alteração de esquema — o workspace permanece na versão anterior em um estado consistente. Isto é intencional: a pré-instalação é sua última chance de recusar uma atualização arriscada.
Exemplo — copiar os valores de um campo legado antes que a migração o remova:
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
// Only the 1.x → 2.x upgrade drops the legacy `notes` field.
if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
return;
}
const client = new CoreApiClient();
const { postCards } = await client.query({
postCards: {
__args: { filter: { notes: { isNot: null } } },
edges: { node: { id: true, notes: true } },
},
});
// Copy legacy `notes` into `description` before the migration drops the
// column. If this fails, the upgrade aborts and the workspace stays on v1.
for (const { node } of postCards.edges) {
await client.mutation({
updatePostCard: {
__args: { id: node.id, data: { description: node.notes } },
id: true,
},
});
}
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Backs up legacy notes into description before the v2 migration.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
</Accordion>
</AccordionGroup>
## Hook de desinstalação
`defineUninstallLogicFunction` declara um hook que é executado quando um usuário desinstala seu app. Ele é executado **antes** que os metadados, dados e código do app sejam removidos — depois que a migration de exclusão é executada, não sobra nada para executar — portanto, seu handler ainda pode consultar os objetos e registros do app. Use-o para limpar recursos externos: desprovisionar recursos de API, excluir bots remanescentes, revogar webhooks.
Notas:
* O hook é de melhor esforço: ele é executado de forma síncrona, mas uma falha é registrada em log e **nunca bloqueia a desinstalação** — a limpeza não deve tornar impossível remover um app.
* Ele recebe `UninstallPayload` (`{ version?: string }` — a versão que está sendo removida).
* Ele **não** é executado quando uma instalação nova com falha é revertida — o app nunca chegou a ser totalmente instalado.
* O hook não pode ser executado depois que o app foi removido, então a limpeza externa que depende de dados do app (por exemplo, IDs de bots armazenados em registros) deve ser feita aqui, não em um job externo agendado.
* Assim como os hooks de instalação, ele **não é executado no modo de desenvolvimento** — em vez disso, acione-o manualmente:
```bash filename="Terminal"
yarn twenty dev:function:exec --uninstall
```
```ts src/logic-functions/uninstall.ts
import { defineUninstallLogicFunction, type UninstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (_payload: UninstallPayload): Promise<void> => {
const client = new CoreApiClient();
const { meetingBots } = await client.query({
meetingBots: { edges: { node: { id: true, externalBotId: true } } },
});
// Delete the provider-side bots so nothing keeps recording after uninstall.
for (const { node } of meetingBots.edges) {
await fetch(`https://api.recorder.example/bots/${node.externalBotId}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${process.env.RECORDER_API_KEY}` },
});
}
};
export default defineUninstallLogicFunction({
universalIdentifier: 'b2c3d4e5-6789-01bc-def0-234567890abc',
name: 'uninstall',
description: 'Deletes remaining recorder bots when the app is uninstalled.',
timeoutSeconds: 300,
handler,
});
```

View File

@@ -0,0 +1,56 @@
---
title: Visão Geral
description: Configure a própria aplicação — a sua identidade, permissões predefinidas e o que é executado no momento da instalação.
icon: screwdriver-wrench
---
A **camada de configuração** de uma aplicação Twenty é o que descreve a aplicação *para a plataforma* — a sua identidade, as permissões que detém e o código que é executado durante a instalação ou atualização. Estas declarações não adicionam novos formatos de dados nem comportamento em tempo de execução; dizem à Twenty *quem é a aplicação* e *como configurá-la*.
```text
┌────────────────────────────────────────────────────────┐
│ Application — identity, default role, variables, │
│ marketplace metadata │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Role — what the app's logic functions can read │ │
│ │ and write (referenced by Application) │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘
│
▼ (at install / upgrade time)
┌──────────────────────────────────┐
│ Pre-install hook │ before metadata migration
└──────────────────────────────────┘
┌──────────────────────────────────┐
│ Post-install hook │ after metadata migration
└──────────────────────────────────┘
│
▼ (at uninstall time)
┌──────────────────────────────────┐
│ Uninstall hook │ before app removal
└──────────────────────────────────┘
```
## Nesta secção
<CardGroup cols={2}>
<Card title="Configuração da aplicação" icon="rocket" href="/l/pt/developers/extend/apps/config/application">
`defineApplication` — identidade, função predefinida, variáveis, metadados do marketplace.
</Card>
<Card title="Funções e Permissões" icon="shield-halved" href="/l/pt/developers/extend/apps/config/roles">
`defineRole` — declara o que as funções de lógica da sua aplicação podem ler e escrever.
</Card>
<Card title="Hooks de instalação" icon="wrench" href="/l/pt/developers/extend/apps/config/install-hooks">
`definePreInstallLogicFunction`, `definePostInstallLogicFunction` e `defineUninstallLogicFunction` — criam cópias de segurança dos dados, inicializam valores predefinidos, validam atualizações e fazem a limpeza na remoção.
</Card>
</CardGroup>
## Como as peças se relacionam
* A **Aplicação** é o ponto de entrada. Cada aplicação tem exatamente uma chamada `defineApplication()`, e esta aponta para uma **Função** como predefinida.
* A **Função** controla o que as funções de lógica e os componentes de front-end da aplicação podem ler e escrever. Siga o princípio do menor privilégio: conceda apenas as permissões de que o seu código realmente necessita.
* Os **Hooks de instalação** são executados durante a instalação ou atualização — a pré-instalação antes da migração de metadados (para que possa recusar uma atualização arriscada) e a pós-instalação após a migração (para que possa inicializar dados predefinidos com base no novo esquema). O hook de desinstalação é executado imediatamente antes de o app ser removido, para que ele possa limpar recursos externos enquanto os dados do app ainda são legíveis.
<Note>
Os hooks de instalação partilham o ambiente de execução de [função de lógica](/l/pt/developers/extend/apps/logic/logic-functions) — a mesma assinatura de handler, as mesmas variáveis de ambiente, o mesmo cliente de API tipado — mas são declarados com as suas próprias funções "define" e vivem fora do modelo de disparo normal (HTTP, cron, eventos de base de dados).
</Note>

View File

@@ -0,0 +1,67 @@
---
title: Recursos públicos
description: Envie arquivos estáticos — imagens, ícones, fontes — junto com seu app por meio da pasta public/.
icon: folder-open
---
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.
* **Usado para metadados de marketplace** — os campos `logo` e `galleryImages` no arquivo de referência `defineApplication()` desta pasta (por exemplo, `public/logo.png`). Eles são exibidos no marketplace quando seu app é publicado. URLs absolutas externas são ignoradas para esses campos — agrupe as imagens em `public/`. Cada imagem não pode exceder 10 MB.
* **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ções** — `yarn twenty dev: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:**
```ts src/logic-functions/send-invoice.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';
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:**
```tsx src/front-components/company-card.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';
const CompanyCard = () => {
const logoUrl = getPublicAssetUrl('logo.png');
return <img src={logoUrl} alt="App logo" />;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'company-card',
component: CompanyCard,
});
```
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.

View File

@@ -0,0 +1,207 @@
---
title: Funções e Permissões
description: Declare quais objetos e campos as funções de lógica e os componentes de front-end do seu app podem ler e gravar.
icon: shield-halved
---
Um **papel** é um conjunto de permissões: quais objetos um app pode ler ou gravar, quais campos ele pode ver e quais recursos em nível de plataforma ele pode usar. Todas as funções de lógica e os componentes de front-end de cada app herdam as permissões do papel marcado com `defineApplicationRole()` (consulte [O papel padrão da função](#the-default-function-role) abaixo).
```ts src/roles/restricted-company-role.ts
import {
defineRole,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
SystemPermissionFlag,
} from 'twenty-sdk/define';
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,
},
],
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS],
});
```
## Segurança ao nível da linha
As permissões de objeto e de campo determinam *quais objetos e campos* uma função pode acessar. **Predicados de permissão ao nível da linha** vão além e determinam *quais registros* uma função pode ver e atuar sobre — por exemplo, uma função de autoatendimento em que cada usuário externo vê apenas seus próprios registros.
Declare predicados com `rowLevelPermissionPredicates` na função. Como o restante do manifesto, cada predicado carrega seu próprio `universalIdentifier` e faz referência a um objeto e a um campo por seus respectivos `universalIdentifier`, a um `operand` e, opcionalmente, a um campo de workspaceMember cujo valor é injetado no momento da consulta — assim, você pode expressar: "a relação de proprietário do registro **é** o membro de workspace atual":
```ts src/roles/partner-role.ts
import {
defineRole,
RowLevelPermissionPredicateOperand,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { ACCOUNT_OWNER_FIELD_UNIVERSAL_IDENTIFIER } from '../fields/account-owner.field';
export default defineRole({
universalIdentifier: 'c3c1dc2e-1a08-4de5-abb7-2139b3d99343',
label: 'Partner',
description: 'External partner — sees only its own records',
canBeAssignedToUsers: true,
objectPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
canReadObjectRecords: true,
canUpdateObjectRecords: true,
},
],
rowLevelPermissionPredicates: [
{
universalIdentifier: 'd0f0c1a2-3b4c-4d5e-8f60-111111111111',
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
fieldUniversalIdentifier: ACCOUNT_OWNER_FIELD_UNIVERSAL_IDENTIFIER,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.workspaceMember.fields.id
.universalIdentifier,
},
],
});
```
Como os predicados são enviados no manifesto, eles são criados, atualizados e removidos junto com a
função em cada instalação e atualização — não há uma etapa pós-instalação separada para manter em sincronia.
### Combinando predicados com grupos
Por padrão, os predicados de uma função são combinados com `AND`. Para combinar alguns deles com `OR` (ou para
aninhar a lógica), declare uma entrada `rowLevelPermissionPredicateGroups` e faça cada predicado apontar para ela por meio de
`predicateGroupUniversalIdentifier`. Essa função permite que um parceiro veja uma Oportunidade da qual ele **é proprietário**
ou pela qual **é o ponto de contato**:
```ts src/roles/partner-opportunities-role.ts
import {
defineRole,
RowLevelPermissionPredicateGroupLogicalOperator,
RowLevelPermissionPredicateOperand,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
const OPPORTUNITY = STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity;
const CURRENT_MEMBER =
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.workspaceMember.fields.id
.universalIdentifier;
export default defineRole({
universalIdentifier: 'b2a1c0d9-8e7f-4a6b-9c5d-222222222222',
label: 'Partner (opportunities)',
canBeAssignedToUsers: true,
objectPermissions: [
{
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
canReadObjectRecords: true,
},
],
rowLevelPermissionPredicateGroups: [
{
universalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator.OR,
},
],
rowLevelPermissionPredicates: [
{
universalIdentifier: 'd4c3b2a1-0e9f-4c8b-9e7d-444444444444',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
fieldUniversalIdentifier: OPPORTUNITY.fields.owner.universalIdentifier,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier: CURRENT_MEMBER,
predicateGroupUniversalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
},
{
universalIdentifier: 'e5d4c3b2-1f0e-4d9c-8f8e-555555555555',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
fieldUniversalIdentifier:
OPPORTUNITY.fields.pointOfContact.universalIdentifier,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier: CURRENT_MEMBER,
predicateGroupUniversalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
},
],
});
```
Notas:
* Dê a cada predicado e grupo um `universalIdentifier` estável (qualquer uuid) — ele identifica a entidade
entre atualizações, e os predicados referenciam os grupos por meio dele.
* Os predicados podem referenciar objetos e campos pertencentes ao seu app ou aos objetos padrão da Twenty.
* A segurança ao nível da linha é aplicada para os workspaces em planos que a incluem; os predicados ainda são sincronizados
em outros planos; eles simplesmente não são aplicados.
## Papel de função padrão
Ao criar um novo app com o scaffold, a CLI cria um arquivo de papel padrão declarado com `defineApplicationRole()`:
```ts src/roles/default-role.ts
import { defineApplicationRole } from 'twenty-sdk/define';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
export default defineApplicationRole({
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: [],
permissionFlagUniversalIdentifiers: [],
});
```
`defineApplicationRole()` é um wrapper leve em torno de `defineRole()` que sinaliza **o** papel usado como padrão do seu app no momento da instalação. A validação é idêntica à de `defineRole`, mas o pipeline de build conecta automaticamente seu `universalIdentifier` ao `defaultRoleUniversalIdentifier` do manifesto do app — assim, você não precisa referenciá-lo em [`defineApplication`](/l/pt/developers/extend/apps/config/application).
Notas:
* Exatamente **um** `defineApplicationRole(...)` é permitido por app — o build do manifesto falhará se encontrar mais de um.
* Use `defineRole()` (não `defineApplicationRole()`) para quaisquer papéis **adicionais** que o seu app forneça.
* Definir `defaultRoleUniversalIdentifier` explicitamente em `defineApplication()` ainda é compatível para retrocompatibilidade, mas foi preterido em favor de `defineApplicationRole()`.
## Melhores Práticas
* Comece a partir do papel gerado pelo scaffold e, em seguida, restrinja-o progressivamente — o padrão concede amplo acesso de leitura, o que raramente é o que você quer em produção.
* Substitua `objectPermissions` e `fieldPermissions` pelos objetos e campos de que suas funções realmente precisam.
* `permissionFlagUniversalIdentifiers` controlam o acesso a recursos em nível de plataforma. Mantenha-os no mínimo necessário.
* Veja um exemplo de trabalho: [`exemplos/hello-world/src/roles/default-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/hello-world/src/roles/default-role.ts).

View File

@@ -0,0 +1,52 @@
---
title: Estendendo objetos
description: Adicione campos a objetos padrão do Twenty (Person, Company, …) ou a objetos de outros apps usando defineField.
icon: wand-magic-sparkles
---
Use `defineField()` para adicionar um campo a um objeto que você não possui — um objeto padrão do Twenty como Person ou Company, ou um objeto disponibilizado por outro app instalado. Ao contrário dos campos inline declarados dentro de [`defineObject`](/l/pt/developers/extend/apps/data/objects), os campos independentes exigem um `objectUniversalIdentifier` para especificar qual objeto eles estendem.
```ts src/fields/company-loyalty-tier.field.ts
import { defineField, FieldType } from 'twenty-sdk/define';
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 do Twenty, importe a constante de `twenty-sdk`:
```ts
import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier
// …
```
* Ao definir campos **inline dentro de `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()`.
* A localização do arquivo fica a seu critério. A convenção é `src/fields/\<name>.field.ts`, mas o SDK detecta campos em qualquer lugar dentro de `src/`.
* A `writability` também pode ser definida por campo: `MetadataWritability.APPLICATION` restringe as gravações às funções de lógica do seu próprio aplicativo, e `MetadataWritability.SYSTEM` é reservado para dados gerenciados pela plataforma. A capacidade de gravação de um campo só pode ser mais restrita do que o nível do seu objeto, e as leituras não são afetadas. Consulte [Objects](/l/pt/developers/extend/apps/data/objects) para a semântica completa.
* Para adicionar uma aba a um layout de página padrão (por exemplo, a página de detalhes de Task ou Company), use [`definePageLayoutTab`](/l/pt/developers/extend/apps/layout/page-layouts#definepagelayouttab) com `STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS` de `twenty-sdk/define`.
## Adicionando uma relação a um objeto existente
Para adicionar um campo de relação (por exemplo, vinculando seu objeto personalizado a um `Person` padrão), use `defineField()` com `FieldType.RELATION`. O padrão é o mesmo que para relações inline, mas com `objectUniversalIdentifier` definido explicitamente. Veja [Relações](/l/pt/developers/extend/apps/data/relations) para o padrão bidirecional.

View File

@@ -0,0 +1,143 @@
---
title: Objetos
description: Declare novos tipos de registro — tabelas personalizadas com seus próprios campos — usando defineObject.
icon: table
---
**Objetos** personalizados são novos tipos de registro que o seu app adiciona a um espaço de trabalho — cartão‑postal, fatura, assinatura, qualquer coisa específica do seu domínio. Cada objeto declara seu esquema (campos, relações, valores padrão) e um identificador universal estável que persiste entre sincronizações e implantações.
```ts src/objects/post-card.object.ts
import { defineObject, FieldType } from 'twenty-sdk/define';
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
* 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.
* `openRecordIn` define onde os registros deste objeto são abertos quando clicados: `ObjectOpenRecordIn.USER_CHOICE` (o padrão, seguindo a preferência de cada membro do workspace em Settings → Experience), `ObjectOpenRecordIn.SIDE_PANEL` ou `ObjectOpenRecordIn.RECORD_PAGE`. Fixe em `RECORD_PAGE` para registros que precisam de uma página inteira para serem utilizáveis, como acontece com fluxos de trabalho e dashboards, ou em `SIDE_PANEL` para registros que só fazem sentido como um painel rápido, como acontece com eventos de calendário.
* `writability` controla quem pode escrever registros do objeto em geral, antes que as permissões de papéis sejam aplicadas: `MetadataWritability.OPEN` (o padrão — os papéis do workspace determinam), `MetadataWritability.APPLICATION` (apenas as próprias funções de lógica do seu app podem criar, atualizar ou excluir registros; use isto para objetos de configuração cujos registros determinam o comportamento, de modo que membros do workspace com amplo acesso a registros não possam editá-los por meio da API) ou `MetadataWritability.SYSTEM` (reservado para dados gerenciados pela plataforma). Leituras não são afetadas — isto é aplicado no lado do servidor, diferentemente de `isUIEditable`, que apenas oculta elementos da interface. Ele também existe por campo, onde só pode ser mais restritivo do que o nível do objeto.
* Campos inline definidos aqui **não** precisam de `objectUniversalIdentifier` — ele é herdado do objeto pai. Use [`defineField()`](/l/pt/developers/extend/apps/data/extending-objects) para adicionar campos a objetos que não pertencem a você.
* Você pode criar novos objetos com `yarn twenty dev:add object`, que orienta você na definição de nomes, campos e relacionamentos. Veja [Arquitetura → Scaffolding de entidades](/l/pt/developers/extend/apps/getting-started/scaffolding).
<Note>
**Os campos base são adicionados automaticamente.** Quando você define um objeto personalizado, o Twenty cria campos padrão como `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` e `deletedAt` para você. Você não precisa declará‑los no seu array `fields` — apenas seus campos personalizados. Você pode substituir um campo padrão declarando um com o mesmo nome, mas isso raramente é uma boa ideia.
</Note>
## Tipos de campo
O conjunto completo de valores de `FieldType`, exportados de `twenty-sdk/define`:
| Categoria | Tipos |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Texto | `TEXT`, `RICH_TEXT`, `ARRAY` (de strings), `RAW_JSON` |
| Numérico | `NUMBER` (`universalSettings.dataType`: `'float'` / `'int'` / `'bigint'`), `NUMERIC` (precisão arbitrária), `RATING`, `POSITION` |
| Datas | `DATE`, `DATE_TIME` |
| Escolha | `BOOLEAN`, `SELECT`, `MULTI_SELECT` |
| Composto | `FULL_NAME`, `ADDRESS`, `EMAILS`, `PHONES`, `LINKS`, `CURRENCY`, `ACTOR`, `FILES` |
| Identificadores e relações | `UUID`, `RELATION`, `MORPH_RELATION` (veja [Relações](/l/pt/developers/extend/apps/data/relations)) |
| Sistema | `TS_VECTOR` (vetor de pesquisa de texto completo, gerenciado pelo servidor) |
Tipos compostos armazenam vários subcampos (por exemplo, `FULL_NAME` = primeiro + último nome; `CURRENCY` = `amountMicros` + `currencyCode`). `SELECT` e `MULTI_SELECT` exigem um array `options`, como no exemplo acima.
## Valores padrão
Valores padrão de strings literais devem ser colocados entre aspas simples **dentro** da string — `defaultValue: "'Draft'"`, não `defaultValue: "Draft"`. É por isso que o campo `status` acima usa `` `'${PostCardStatus.DRAFT}'` ``.
Strings sem aspas são reservadas para valores padrão computados, avaliados quando um registro é criado:
* `'uuid'` — gera um UUID (para campos `UUID`)
* `'now'` — o carimbo de data/hora atual (para campos `DATE_TIME`)
A mesma convenção se aplica a subcampos de string de valores padrão compostos (por exemplo, `{ source: "'MANUAL'" }` em um campo `ACTOR`) e a valores de `SELECT`/`MULTI_SELECT`. Uma string literal padrão deixada sem aspas gera um aviso quando seu app é compilado.
## Nulabilidade
`isNullable` controla se um campo aceita `NULL`. O valor padrão é `true` — omita-o para campos opcionais. Defina `isNullable: false` para tornar um campo obrigatório no nível do banco de dados.
Alterações em `isNullable` são aplicadas em todas as sincronizações, incluindo sincronizações que atualizam um campo existente — assim, você pode alternar a nulabilidade de um campo editando o manifesto e sincronizando novamente.
<Note>
**Tornar um campo existente obrigatório requer um valor padrão.** Quando você altera um campo para `isNullable: false`, também deve fornecer um `defaultValue` não nulo. O valor padrão preenche retroativamente quaisquer linhas `NULL` existentes antes de a restrição `NOT NULL` ser aplicada; sem ele a sincronização falha com `Default value cannot be null for non-nullable fields`. Campos de relação e campos `TS_VECTOR` sempre aceitam nulos, portanto `isNullable` não tem efeito sobre eles.
</Note>
```ts
{
universalIdentifier: 'b1a7c0de-1234-4f00-9abc-000000000000',
name: 'reference',
type: FieldType.TEXT,
label: 'Reference',
isNullable: false,
defaultValue: "'N/A'",
}
```
## O que vem depois
* **Conecte este objeto a outros** — veja [Relações](/l/pt/developers/extend/apps/data/relations) para o padrão de relação bidirecional.
* **Adicione campos a objetos de outros apps** — veja [Extensão de objetos](/l/pt/developers/extend/apps/data/extending-objects) para `defineField()`.
* **Exibir este objeto na interface do usuário (UI)** — consulte [Itens do menu de navegação](/l/pt/developers/extend/apps/layout/navigation-menu-items) para adicionar uma entrada na barra lateral; consulte [Views](/l/pt/developers/extend/apps/layout/views) para adicionar configurações de lista personalizadas.

View File

@@ -0,0 +1,101 @@
---
title: Visão Geral
description: Modele os dados que seu app adiciona a um workspace — objetos, campos e relações.
icon: database
---
A **camada de dados** de um app Twenty é o conjunto de dados que seu app *adiciona* a um workspace — os novos tipos de registros que ele declara, as colunas que adiciona a objetos existentes e como esses registros se conectam entre si.
```text
┌──────────────────────────────────────────────────┐
│ Object — a record type, e.g. PostCard │
│ ├─ Field (name, type, label) │
│ ├─ Field │
│ └─ Relation (link to another object) │
└──────────────────────────────────────────────────┘
│
├── lives in your app, OR
│
▼
┌──────────────────────────────────────────────────┐
│ Standard / other apps' objects │
│ └─ Field added by your app via defineField │
└──────────────────────────────────────────────────┘
```
## Nesta seção
<CardGroup cols={2}>
<Card title="Objetos" icon="table" href="/l/pt/developers/extend/apps/data/objects">
`defineObject` — declare novos tipos de registros com seus próprios campos.
</Card>
<Card title="Estendendo objetos" icon="wand-magic-sparkles" href="/l/pt/developers/extend/apps/data/extending-objects">
`defineField` — adicione campos a objetos padrão ou de outros apps.
</Card>
<Card title="Relações" icon="diagram-project" href="/l/pt/developers/extend/apps/data/relations">
Conexões bidirecionais `MANY_TO_ONE` / `ONE_TO_MANY` entre objetos.
</Card>
<Card title="Tipos de atividade da cronologia" icon="clock-rotate-left" href="/l/pt/developers/extend/apps/data/timeline-activity-types">
Defina eventos de auditoria automáticos e eventos explícitos do app para linhas do tempo de registros.
</Card>
</CardGroup>
## Entidades em resumo
| Entidade | Finalidade | Definido com |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------- |
| **Objeto** | Um novo tipo de registro personalizado (por exemplo, PostCard, Invoice) com seus próprios campos | `defineObject()` |
| **Campo** | Uma coluna em um objeto. Campos independentes podem estender objetos que você não criou (por exemplo, adicionar `loyaltyTier` ao objeto Company) | `defineField()` |
| **Relação** | Um vínculo bidirecional entre dois objetos — ambos os lados declarados como campos | `defineField()` com `FieldType.RELATION` |
| **Índice** | Um índice de banco de dados para acelerar uma consulta recorrente em um dos seus objetos | `defineIndex()` |
| **Tipo de atividade da cronologia** | Um evento de auditoria automático ou evento de domínio explícito exibido nas linhas do tempo de registros | `defineTimelineActivityType()` |
O SDK detecta esses elementos por meio de análise de AST em tempo de build, então a organização dos arquivos fica a seu critério — a convenção é `src/objects/`, `src/fields/`, `src/indexes/` e `src/timeline-activity-types/`. UUIDs `universalIdentifier` estáveis conectam tudo em implantações diferentes.
## Índices (Opcional)
Os apps podem incluir índices junto com seus objetos para manter rápidas as consultas recorrentes. O caso mais comum é uma coluna de status ou de chave estrangeira que você lê com frequência.
```ts src/indexes/post-card-status.index.ts
import { defineIndex } from 'twenty-sdk/define';
import {
POST_CARD_UNIVERSAL_IDENTIFIER,
STATUS_FIELD_UNIVERSAL_IDENTIFIER,
} from '../objects/post-card.object';
export default defineIndex({
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff0',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
fields: [
{
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff1',
fieldUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
},
],
});
```
### Índices únicos
`defineIndex` aceita `isUnique: true` tanto para unicidade de uma única coluna quanto de múltiplas colunas. Este é o recurso recomendado — `defineField({ isUnique: true })` está obsoleto e será removido em uma versão futura.
```ts
defineIndex({
universalIdentifier: '…',
objectUniversalIdentifier: PERSON_UNIVERSAL_IDENTIFIER,
isUnique: true,
fields: [{ universalIdentifier: '…', fieldUniversalIdentifier: EMAIL_FIELD_UNIVERSAL_IDENTIFIER }],
});
```
### Outras restrições
* Cláusulas `WHERE` parciais permanecem sob controle do administrador — os apps não podem declará-las.
* Cada objeto é limitado a 10 índices personalizados (os índices do próprio framework não contam).
Ordene o array `fields` da forma como o Postgres deve usá-lo — coluna mais à esquerda primeiro, como em uma lista telefônica. Índices não são gratuitos: cada gravação na tabela os atualiza. Adicione um apenas quando você tiver uma consulta que precise dele.
<Note>
Procurando por **Application Config** ou **Roles & Permissions**? Esses descrevem o próprio app em vez dos dados que ele adiciona — eles ficam em [Config](/l/pt/developers/extend/apps/config/overview). Procurando por **Connections** (Linear, GitHub, Slack OAuth)? Essas existem para serem chamadas *a partir de* funções de lógica e ficam em [Logic](/l/pt/developers/extend/apps/logic/connections).
</Note>

View File

@@ -0,0 +1,184 @@
---
title: Relações
description: Conecte objetos entre si com relações bidirecionais MANY_TO_ONE / ONE_TO_MANY.
icon: diagram-project
---
As relações conectam dois objetos entre si. No Twenty, as relações são sempre **bidirecionais** — cada relação tem dois lados, e cada lado é declarado como um campo que faz referência ao outro.
| Tipo de relação | Descrição | Tem chave estrangeira? |
| --------------- | ----------------------------------------------------------------- | ---------------------- |
| `MANY_TO_ONE` | Muitos registros deste objeto apontam para um registro do destino | Sim (`joinColumnName`) |
| `ONE_TO_MANY` | Um registro deste objeto possui muitos registros do destino | Não (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
Um `PostCard` pode 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"):
```ts src/fields/post-card-recipients-on-post-card.field.ts
import { defineField, FieldType, RelationType } from 'twenty-sdk/define';
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):
```ts src/fields/post-card-on-post-card-recipient.field.ts
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk/define';
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',
},
});
```
<Note>
**Importações circulares:** ambos os campos de relação fazem referência ao `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. O sistema de build resolve isso em tempo de compilação.
</Note>
## Relações de junção
Um objeto de junção conecta um registro de origem a um ou mais registros de destino. Por exemplo, `PostCardRecipient` pode conter tanto uma relação `postCard` quanto uma relação `recipient`. No campo `ONE_TO_MANY` do objeto de origem, defina `junctionTargetFieldUniversalIdentifier` como o campo de relação no objeto de junção que aponta para o destino:
```ts
export default defineField({
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCardRecipients',
label: 'Post Card Recipients',
relationTargetObjectMetadataUniversalIdentifier:
POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
junctionTargetFieldUniversalIdentifier: RECIPIENT_FIELD_ID,
},
});
```
`RECIPIENT_FIELD_ID` deve identificar uma relação `MANY_TO_ONE` ou morph no objeto de junção. Isso informa ao Twenty qual registro relacionado é o destino quando um recurso como [distribuição de atividade da linha do tempo](/l/pt/developers/extend/apps/data/timeline-activity-types#automatic-and-explicit-events) percorre a junçã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`:
```ts src/fields/person-on-self-hosting-user.field.ts
import {
defineField,
FieldType,
RelationType,
OnDeleteAction,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
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
| Propriedade | Obrigatório | Descrição |
| ---------------------------------------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `type` | Sim | Deve ser `FieldType.RELATION` |
| `relationTargetObjectMetadataUniversalIdentifier` | Sim | O `universalIdentifier` do objeto de destino |
| `relationTargetFieldMetadataUniversalIdentifier` | Sim | O `universalIdentifier` do campo correspondente no objeto de destino |
| `universalSettings.relationType` | Sim | `RelationType.MANY_TO_ONE` ou `RelationType.ONE_TO_MANY` |
| `universalSettings.onDelete` | Apenas para MANY_TO_ONE | O que acontece quando o registro referenciado é excluído: `CASCADE`, `SET_NULL`, `RESTRICT` ou `NO_ACTION` |
| `universalSettings.joinColumnName` | Apenas para MANY_TO_ONE | Nome da coluna no banco de dados para a chave estrangeira (por exemplo, `postCardId`) |
| `universalSettings.junctionTargetFieldUniversalIdentifier` | Somente ONE_TO_MANY de junção | Campo de relação no objeto de junção que aponta para o destino da distribuição |
## Campos de relação inline
Você também pode declarar uma relação diretamente dentro de [`defineObject`](/l/pt/developers/extend/apps/data/objects). Quando estiver inline, omita `objectUniversalIdentifier` — ele é herdado do objeto pai:
```ts
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
],
});
```

View File

@@ -0,0 +1,212 @@
---
title: Referenciando metadados de sistema
description: Resolva os identificadores universais determinísticos dos metadados que o Twenty provisiona automaticamente em cada objeto, para que seu app possa referenciá-los sem hardcoding.
icon: gears
---
Todo objeto no Twenty vem com **metadados de sistema** que você nunca declara explicitamente, como um conjunto de campos e uma visualização principal de lista com suas colunas. O servidor cria tudo isso quando o objeto é provisionado, e o conjunto cresce conforme o Twenty cresce.
Como você não o declara, não existe nenhuma constante `universalIdentifier` para você importar. Em vez disso, o servidor **deriva** cada identificador de forma determinística, e o `twenty-sdk` expõe a mesma derivação para que seu manifesto possa resolver o valor exato que o servidor usa.
## Campos do sistema
Os campos escalares presentes em todo objeto, nenhum dos quais você declara com [`defineField()`](/l/pt/developers/extend/apps/data/extending-objects):
`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`
Então, como você referencia `createdAt` como uma coluna em uma [visualização](/l/pt/developers/extend/apps/layout/views)?
### O Problema
Desde o Twenty 2.19, o identificador universal de um campo de sistema é **derivado deterministicamente** pelo servidor a partir de três entradas: o identificador universal do aplicativo, o identificador universal do objeto e o nome do campo. Inventar um id e deixá-lo hardcoded não funciona: ele não corresponde a nada no servidor, e a sincronização rejeita a referência pendente:
```
Dev sync failed: viewField: INVALID_VIEW_DATA: Field metadata not found
```
### A Solução
<Note>
`getFieldUniversalIdentifier` está disponível a partir do `twenty-sdk` 2.21.
</Note>
Use `getFieldUniversalIdentifier` para resolver exatamente o mesmo valor que o servidor usa. Ela recebe as três entradas e retorna o identificador universal do campo:
```ts
import { getFieldUniversalIdentifier } from 'twenty-sdk/define';
const createdAtFieldId = getFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
name: 'createdAt',
});
```
* `applicationUniversalIdentifier` é o identificador do seu app, aquele que você passa para [`defineApplication()`](/l/pt/developers/extend/apps/config/application).
* `objectUniversalIdentifier` é o identificador do objeto ao qual o campo pertence.
* `name` é o nome do campo de sistema, um dos valores listados acima.
### Exemplo: uma coluna createdAt em uma visualização
O caso típico é adicionar uma coluna `createdAt` a uma visualização de um dos seus objetos personalizados. Resolva o id do campo e referencie-o como qualquer outro `fieldMetadataUniversalIdentifier`:
```ts src/views/example-view.ts
import {
defineView,
getFieldUniversalIdentifier,
} from 'twenty-sdk/define';
const APPLICATION_UNIVERSAL_IDENTIFIER =
'0b04e15c-27b2-4741-9046-b32e07469072';
const MY_OBJECT_UNIVERSAL_IDENTIFIER =
'c782b61c-70fd-4c88-9cd6-4e61ab8d7591';
export default defineView({
universalIdentifier: '70f10d44-144a-4da8-8c6f-3ec2422138c0',
name: 'All records',
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
position: 0,
fields: [
{
universalIdentifier: '75a90bc4-d901-4df4-85e0-af29db5e0104',
fieldMetadataUniversalIdentifier: getFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: MY_OBJECT_UNIVERSAL_IDENTIFIER,
name: 'createdAt',
}),
position: 0,
isVisible: true,
size: 200,
},
],
});
```
O mesmo id resolvido funciona em qualquer lugar onde se espera um `fieldMetadataUniversalIdentifier`: campos de visualização, filtros, ordenações, agrupamentos e widgets de layout de página.
<Note>
Resolva o id, não o deixe hardcoded. Como o servidor deriva o valor a partir do
id do aplicativo, do id do objeto e do nome do campo, chamar
`getFieldUniversalIdentifier` mantém a sua referência correta mesmo que essas
entradas mudem, e evita divergências se a derivação evoluir no futuro.
</Note>
### Campos de relação do sistema
<Note>
`getSystemRelationFieldUniversalIdentifier` está disponível a partir da versão 2.23 do `twenty-sdk` e requer um servidor Twenty na versão 2.23 ou posterior.
</Note>
Além dos campos escalares de sistema acima, o servidor também provisiona quatro **campos de relação de sistema** em cada objeto: `timelineActivities`, `attachments`, `noteTargets` e `taskTargets`, cada um apontando para o objeto de relação padrão correspondente.
Dessa forma, esses campos não são resolvidos com `getFieldUniversalIdentifier`: seu identificador é derivado de forma **independente de nome**, a partir do objeto que hospeda o campo e do objeto para o qual o campo aponta. Dessa forma, renomear um objeto nunca altera os identificadores de seus campos de relação.
Use `getSystemRelationFieldUniversalIdentifier` para resolvê-los:
```ts
import {
getSystemRelationFieldUniversalIdentifier,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
// rocket.attachments — the relation field hosted on your custom object
const rocketAttachmentsFieldId = getSystemRelationFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
relationTargetObjectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.attachment.universalIdentifier,
});
```
* `objectUniversalIdentifier` é o objeto que **hospeda** o campo.
* `relationTargetObjectUniversalIdentifier` é o objeto para o qual o campo **aponta**.
A direção é codificada pela ordem dos argumentos. Para resolver o lado inverso (por exemplo, `attachment.targetRocket`, o campo morph que o servidor cria no objeto de relação padrão), inverta os dois:
```ts
// attachment.targetRocket — the reverse morph field on Attachment
const attachmentTargetRocketFieldId =
getSystemRelationFieldUniversalIdentifier({
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.attachment.universalIdentifier,
relationTargetObjectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
});
```
Assim como acontece com campos de sistema escalares, o id resolvido funciona em qualquer lugar em que um `fieldMetadataUniversalIdentifier` é esperado.
## Visualizações de sistema
<Note>
`getSystemViewUniversalIdentifier` e `getSystemViewFieldUniversalIdentifier`
estão disponíveis a partir da versão 2.26 do `twenty-sdk` e requerem um servidor Twenty na versão 2.26 ou posterior.
</Note>
O servidor também provisiona uma **visualização de sistema** em cada objeto: a visualização principal de lista (`All {objectLabelPlural}`, com chave `ViewKey.INDEX` (derivada com `SYSTEM_VIEW_KEYS.INDEX`)), com uma coluna por campo exibível. Assim como os campos de relação de sistema, seus identificadores são derivados **sem usar nomes**, então renomear um objeto ou um campo nunca os altera.
Use `getSystemViewUniversalIdentifier` para resolvê‑la:
```ts
import {
getSystemViewUniversalIdentifier,
SYSTEM_VIEW_KEYS,
} from 'twenty-sdk/define';
const rocketIndexViewId = getSystemViewUniversalIdentifier({
objectMetadataApplicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: ROCKET_OBJECT_UNIVERSAL_IDENTIFIER,
viewKey: SYSTEM_VIEW_KEYS.INDEX,
});
```
* `objectMetadataApplicationUniversalIdentifier` é o aplicativo proprietário do **objeto**, que define o namespace da visualização.
* `objectUniversalIdentifier` é o objeto que a visualização lista.
* `viewKey` é o discriminador de visualização de sistema: `SYSTEM_VIEW_KEYS.INDEX` para a visualização principal de lista, `SYSTEM_VIEW_KEYS.FIELDS_WIDGET` para a visualização do widget de campos da página de registro. Ele é a chave da derivação; apenas `INDEX` também é armazenado na linha da visualização.
O id resolvido funciona em qualquer lugar em que um `viewUniversalIdentifier` seja esperado, como uma entrada de barra lateral [`NavigationMenuItemType.VIEW`](/l/pt/developers/extend/apps/layout/navigation-menu-items). Para simplesmente abrir a lista principal de um objeto, prefira `NavigationMenuItemType.OBJECT` com `targetObjectUniversalIdentifier`: isso não precisa de derivação.
`getSystemViewFieldUniversalIdentifier` resolve uma única **coluna** em uma visualização de sistema, a partir da visualização e do campo que ela exibe:
```ts
import { getSystemViewFieldUniversalIdentifier } from 'twenty-sdk/define';
const rocketNameColumnId = getSystemViewFieldUniversalIdentifier({
fieldMetadataApplicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier: rocketIndexViewId,
fieldMetadataUniversalIdentifier: ROCKET_NAME_FIELD_UNIVERSAL_IDENTIFIER,
});
```
Observe o primeiro argumento: uma coluna é colocada em namespace pelo aplicativo proprietário do **campo que ela exibe**, não pelo que é proprietário da visualização. Um campo que seu app adiciona a um objeto padrão tem sua coluna derivada sob o seu aplicativo, em uma visualização de propriedade do Twenty.
<Warning>
As visualizações de sistema e suas colunas são **de propriedade do servidor**: resolva seus identificadores
para referenciá‑las, nunca para declará‑las. `key` em
[`defineView()`](/l/pt/developers/extend/apps/layout/views) está obsoleto e
é ignorado, portanto uma visualização de manifesto nunca pode reivindicar a chave `INDEX`, e o servidor
já provisiona uma coluna para cada campo que você adiciona, então declarar seu próprio
`defineViewField()` para esse mesmo campo em uma visualização de sistema entra em conflito com ela.
</Warning>
## Objetos Padrão do Twenty
Para um objeto **padrão** do Twenty (Person, Company, Opportunity, …), você não precisa derivar nada: os identificadores são constantes pré‑computadas que você pode importar diretamente, tanto para campos quanto para visualizações.
```ts
import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.createdAt.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.fields.updatedAt.universalIdentifier
// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.views.allPeople.universalIdentifier
```
Use os helpers acima quando o objeto for um que **seu app** define com [`defineObject()`](/l/pt/developers/extend/apps/data/objects), em que não existe tal constante.
<Note>
`name` é um campo **padrão**, não um campo de sistema. Ele mantém seu próprio identificador universal
hardcoded e não é resolvido por meio de
`getFieldUniversalIdentifier`. Em objetos que você define, referencie o campo
`name` pelo identificador que você atribuiu a ele em `defineObject()`.
</Note>

View File

@@ -0,0 +1,184 @@
---
title: Tipos de Atividade Linha do Tempo
description: Define eventos de auditoria automáticos e eventos explícitos do aplicativo que renderizam em linhas de tempo registradas.
icon: clock-rotate-left
---
Um tipo de atividade de linha do tempo define o vocabulário estável para um evento mostrado na linha do tempo de um registro. Objetos padrão e objetos de aplicativos usam o mesmo contrato: um tipo tem um rótulo e ícone, podem opcionalmente declarar quando é emitido, e pode opcionalmente renderizar através de um dos [componentes frontais da sua aplicação](/l/pt/developers/extend/apps/layout/front-components).
<Note>
Os tipos de atividade da linha do tempo estão em beta e em breve em Vinte e 2.34. A API
pode evoluir, enquanto aprendemos com os casos de uso de desenvolvedores de aplicativos.
</Note>
Crie um com o scaffolder:
```bash filename="Terminal"
yarn twenty dev:add timelineActivityType
```
Ou defina-o diretamente:
```ts filename="src/timeline-activity-types/post-card-created.ts"
import { defineTimelineActivityType } from 'twenty-sdk/define';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7500',
name: 'postCardCreated',
label: 'created a post card',
icon: 'IconMail',
emit: {
on: 'created',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
},
});
```
## Eventos automáticos e explícitos
Adicione `emiss` quando Vinte deveria criar este tipo automaticamente. `emit.on` suporta `created`, `updated`, `deleted`, `restored`, `linked`, and `unlinked`, while `emit.objectUniversalIdentifier` identifica o objeto de origem. Apenas um tipo eficaz pode lidar com a mesma chave de emissão —`on`, objeto e opcional através de uma relação—em uma área de trabalho.
Sem `emit.through`, o evento está escrito na própria linha do tempo do registro de origem. Para fã para registros relacionados, defina `emit.through. elationFieldUniversalIdentifier` para uma relação muita-a-dia ou uma [relação de junção] (/developers/extend/apps/data/relations#junction-relations) direta sobre o objeto fonte. As relações morfológicas são favoráveis a todos os membros do seu grupo morfol; portanto, uma única declaração pode visar vários tipos de objeto.
Para uma relação direta, criar ou restaurar o registro fonte produz `linkado`, enquanto ele produz `unlinked`, e reapontando a relação produz `não ligado` no alvo anterior mais `ligado` no novo alvo. Outras atualizações de origem não produzem eventos de ligação. Este é o contrato usado pelos anexos, cujo alvo é uma relação de morph directo.
Para uma relação de junção, `universalSettings.junctionTargdUniversalIdentifier` identifica a relação do objeto de junção com o alvo. Criar ou excluir uma linha de junção produz `ligado` ou `unlinked`. Reapontando qualquer lado de uma linha de junção também produz um evento de link; atualizações para campos de junção não relacionam.
Eventos `ligados` e `não ligados` requerem `emit.through` porque seu gatilho é uma alteração na relação de junção ou direta configurada.
Por exemplo, este é o mesmo contrato genérico usado por notas, tarefas, mensagens e eventos do calendário:
```ts filename="src/timeline-activity-types/post-card-linked.ts"
import { defineTimelineActivityType } from 'twenty-sdk/define';
import { POST_CARD_RECIPIENTS_FIELD_UNIVERSAL_IDENTIFIER } from '../fields/post-card-recipients-on-post-card.field';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7502',
name: 'postCardLinked',
label: 'received a post card',
icon: 'IconMail',
emit: {
on: 'linked',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
through: {
relationFieldUniversalIdentifier:
POST_CARD_RECIPIENTS_FIELD_UNIVERSAL_IDENTIFIER,
},
},
});
```
Um evento `atualizado` é emitido para cada atualização de fonte por padrão. Defina `emit.through.triggerFieldUniversalIdentifiers` quando apenas alterações para os campos de origem selecionados devem aparecer nas linhas do tempo de destino.
Omita `emit` para um evento de domínio explícito que sua função lógica cria a si mesma. Isto evita a elaboração de uma linha de auditoria automática e de uma linha explícita para a mesma operação.
```ts filename="src/timeline-activity-types/post-card-sent.ts"
import { defineTimelineActivityType } from 'twenty-sdk/define';
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
name: 'postCardSent',
label: 'sent a post card',
icon: 'IconSend',
});
```
Criar eventos explícitos com [`createTimelineActivity()`](/l/pt/developers/extend/apps/logic/logic-functions#create-a-timeline-activity). O código do aplicativo usa identificadores universais estáveis; Vinte resolvem os IDs de metadados específicos de instalação.
## Renderização personalizada
Sem um componente frontal, Vinte renderiza uma linha genérica do rótulo de tipos, ícone e metadados do objeto vinculado. Isto funciona para objetos padrão e personalizados sem um renderizador específico do objeto.
Para detalhes personalizados, defina `frontComponentUniversalIdentifier` para um componente frontal pertencente ao mesmo aplicativo:
```ts
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
name: 'postCardSent',
label: 'sent a post card',
icon: 'IconSend',
frontComponentUniversalIdentifier: '88c15ae2-5f87-4a6b-b48f-1974bbe62eb7',
});
```
O cordão nativo continua a ser a apresentação desmoronada. Vinte montam o componente frontal apenas depois que o usuário se expande, evitando uma sandbox e trabalhador para cada evento visível. Dentro do componente, chame `useTimelineActivityId()` de `vinte e sdk/front-component` para ler a ID da linha e buscar quaisquer dados que sua apresentação precisar. Ele retorna `null` quando o componente é renderizado fora de uma linha do tempo.
## Sobrescrevendo a apresentação de outro aplicativo
Um aplicativo possui contratos automáticos de linha do tempo para seus próprios objetos. Para personalizar um evento em um objeto de propriedade de outro aplicativo, declare o tipo existente explicitamente com `replacesTimelineActivityTypeUniversalIdentifier`:
```ts
export default defineTimelineActivityType({
universalIdentifier: 'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7503',
name: 'companyCreatedWithDeploymentContext',
label: 'was created from a deployment',
emit: {
on: 'created',
objectUniversalIdentifier: COMPANY_UNIVERSAL_IDENTIFIER,
},
replacesTimelineActivityTypeUniversalIdentifier:
RECORD_CREATED_TIMELINE_ACTIVITY_TYPE_UNIVERSAL_IDENTIFIER,
frontComponentUniversalIdentifier: '88c15ae2-5f87-4a6b-b48f-1974bbe62eb7',
});
```
Uma substituição deve incluir `emiss`, porque ela substitui um slot de emissão automática existente ao invés de um tipo apenas explícito. O tipo referenciado deve pertencer à aplicação do objeto de destino e descrever a mesma ação e rota. Remover o aplicativo que substitui restaura o tipo base; remover o tipo base desativa a substituição.
## Área de trabalho substitui e silencia
Administradores da Área de Trabalho podem alterar a apresentação de um tipo ou silenciar seus eventos automáticos e explícitos sem bifurcar o aplicativo. Use a mutação do `updateTimelineActivityType` da API com o ID do tipo específico de instalação:
```graphql
mutation CustomizeTimelineActivityType(
$id: UUID!
$label: String
$icon: String
$isActive: Boolean
) {
updateTimelineActivityType(
input: { id: $id, label: $label, icon: $icon, isActive: $isActive }
) {
id
label
icon
isActive
}
}
```
'label' e 'icon' são armazenados como sobreposição de espaço de trabalho, então atualizações posteriores do aplicativo não substituem as escolhas do administrador. Coloque `isActive` como `false` para parar a emissão automática e rejeitar novos eventos explícitos desse tipo. Linhas existentes permanecem visíveis.
Restaure os padrões do aplicativo, incluindo o estado ativo, com:
```graphql
mutation ResetTimelineActivityType($id: UUID!) {
resetTimelineActivityType(id: $id) {
id
label
icon
isActive
}
}
```
## Campos de configuração
| Campo | Obrigatório | Descrição |
| ------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------ |
| `universalIdentifier` | Sim | UUID Estável para o tipo entre instalações e melhorias |
| `name` | Sim | Nome programático local do aplicativo; deve ser exclusivo dentro do aplicativo |
| `label` | Sim | Texto de ação para o usuário usado pelo renderizador nativo |
| `icon` | Não | Nome de 20 ícones exibido ao lado do evento |
| `emitir` | Não | Declaração automática de emissão; omitir para tipos explícitos |
| `emit.on` | Com emissão | Ação de auditoria que faz com que este tipo seja escrito |
| `emit.objectUniversalIdentifier` | Com emissão | Objeto cujo registros emitem este tipo |
| `emit.through.relationFieldUniversalIdentifier` | Não | Relação direta ou de cruzamento usada para ventiladores com cronogramas relacionados |
| `emit.through.triggerFieldUniversalIdentifiers` | Não | Campos de origem que podem acionar um `atualizado` através do evento |
| `frontComponentUniversalIdentifier` | Não | Componente frontal do App-owned montado quando a linha é expandida |
| `replacesTimelineActivityTypeUniversalIdentifier` | Não | Tipo de emissão existente substituído; requer `emit` |
Vinte instantâneos semânticos identidade e apresentação durável fallbacks quando um evento é criado. Linhas históricas mantêm suas ações originais e significados por objetos. Enquanto o tipo está instalado, seu rótulo traduzido, ícone e frontal componente renderizam ao vivo; depois de desinstalado, o snapshot mantém a linha legível. Resolução usa o identificador universal, e a apresentação também sobrevive à reinstalação do aplicativo.

View File

@@ -0,0 +1,102 @@
---
title: Conceitos
description: Como as aplicações Twenty funcionam — modelo de entidade, sandboxing e ciclo de vida da instalação.
icon: sitemap
---
As aplicações Twenty são pacotes TypeScript que estendem seu espaço de trabalho com objetos personalizados, lógica, componentes de UI e recursos de IA. Elas são executadas na plataforma Twenty com sandboxing completo e controles de permissão.
## Como as aplicações funcionam
Uma aplicação é uma coleção de **entidades** declaradas usando funções `defineEntity()` do pacote `twenty-sdk`. O SDK detecta essas declarações via análise de AST no momento da compilação e produz um **manifesto** — uma descrição completa do que seu aplicativo adiciona a um espaço de trabalho. Essas funções validam sua configuração em tempo de compilação e oferecem autocompletar na IDE e segurança de tipos.
```
your-app/
├── src/
│ ├── application-config.ts ← defineApplication (required, one per app)
│ ├── roles/ ← defineRole
│ ├── objects/ ← defineObject
│ ├── fields/ ← defineField
│ ├── logic-functions/ ← defineLogicFunction
│ ├── front-components/ ← defineFrontComponent
│ ├── skills/ ← defineSkill
│ ├── agents/ ← defineAgent
│ ├── views/ ← defineView
│ ├── navigation-menu-items/ ← defineNavigationMenuItem
│ └── page-layouts/ ← definePageLayout
├── public/ ← Static assets (images, icons)
└── package.json
```
<Note>
**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. A estrutura de pastas acima é uma convenção, não um requisito.
</Note>
## Tipos de entidade
| Entidade | Finalidade | Documentação |
| ------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| **Aplicação** | Identidade da aplicação, função padrão, variáveis | [Configuração da aplicação](/l/pt/developers/extend/apps/config/application) |
| **Papel** | Conjuntos de permissões para objetos e campos | [Papéis e permissões](/l/pt/developers/extend/apps/config/roles) |
| **Objeto** | Tipos de registro personalizados com campos | [Objetos](/l/pt/developers/extend/apps/data/objects) |
| **Campo** | Adicionar campos a objetos de outros apps | [Extensão de objetos](/l/pt/developers/extend/apps/data/extending-objects) |
| **Relação** | Links bidirecionais entre objetos | [Relações](/l/pt/developers/extend/apps/data/relations) |
| **Função lógica** | TypeScript no lado do servidor com gatilhos | [Funções lógicas](/l/pt/developers/extend/apps/logic/logic-functions) |
| **Habilidade** | Instruções reutilizáveis para agentes de IA | [Habilidades e Agentes](/l/pt/developers/extend/apps/logic/skills-and-agents) |
| **Agente** | Assistentes de IA com prompts personalizados | [Habilidades e Agentes](/l/pt/developers/extend/apps/logic/skills-and-agents) |
| **Provedor de conexão** | Credenciais OAuth para APIs de terceiros | [Conexões](/l/pt/developers/extend/apps/logic/connections) |
| **Vista** | Vistas de lista de registros pré-configuradas | [Vistas](/l/pt/developers/extend/apps/layout/views) |
| **Item do menu de navegação** | Entradas personalizadas na barra lateral | [Itens do menu de navegação](/l/pt/developers/extend/apps/layout/navigation-menu-items) |
| **Layout da Página** | Abas e widgets na página de detalhes de um registro | [Layouts de página](/l/pt/developers/extend/apps/layout/page-layouts) |
| **Componente de front-end** | UI React em sandbox dentro do Twenty | [Componentes de front-end](/l/pt/developers/extend/apps/layout/front-components) |
| **Dependências compartilhadas** | Dependências compartilhadas pelos componentes de front-end do app, declaradas em package.json | [Dependências compartilhadas](/l/pt/developers/extend/apps/layout/front-components#sharing-dependencies-across-front-components) |
| **Item do menu de comandos** | Ações rápidas e entradas Cmd+K | [Itens do menu de comandos](/l/pt/developers/extend/apps/layout/command-menu-items) |
## Sandboxing
* **Funções lógicas** são executadas em processos Node.js isolados no servidor. Eles acessam dados apenas por meio do cliente de API tipado, que atua como a pessoa que disparou a execução ou como o próprio app. Consulte [Qual acesso uma chamada usa](/l/pt/developers/extend/apps/logic/logic-functions#whose-access-a-call-uses).
* **Componentes de front-end** executam em Web Workers usando Remote DOM — isolados da página principal, mas renderizando elementos DOM nativos (não iframes). Eles se comunicam com o Twenty por meio de uma API de host com passagem de mensagens.
* **Permissões** são aplicadas no nível da API. Dois tokens de runtime são inseridos, ambos derivados da função declarada com `defineApplicationRole()`; o padrão é ainda mais limitado ao que a pessoa que disparou a execução pode fazer quando há alguém por trás da execução.
## Ciclo de vida do aplicativo
```
┌─────────────────────────────────────────────────────────┐
│ Development │
│ npx create-twenty-app → yarn twenty dev (live sync) │
├─────────────────────────────────────────────────────────┤
│ Build & Deploy │
│ yarn twenty dev:build → yarn twenty app:publish │
├─────────────────────────────────────────────────────────┤
│ Install flow │
│ upload → [pre-install] → metadata migration → │
│ generate SDK → [post-install] → … → [uninstall] │
├─────────────────────────────────────────────────────────┤
│ Publish │
│ npm publish → appears in Twenty marketplace │
└─────────────────────────────────────────────────────────┘
```
* **`yarn twenty dev`** — observa seus arquivos-fonte e sincroniza ao vivo as alterações com um servidor Twenty conectado. O cliente de API tipado é regenerado automaticamente quando o esquema muda.
* **`yarn twenty dev:build`** — compila TypeScript, empacota funções de lógica e componentes de front-end com o esbuild e produz um manifesto.
* **Hooks de pré/pós-instalação e desinstalação** — funções opcionais que são executadas durante a instalação ou logo antes da remoção. Veja [Hooks de instalação](/l/pt/developers/extend/apps/config/install-hooks) para detalhes.
## Próximos passos
<CardGroup cols={2}>
<Card title="Configuração" icon="screwdriver-wrench" href="/l/pt/developers/extend/apps/config/overview">
Identidade da aplicação, função padrão e hooks de instalação e desinstalação.
</Card>
<Card title="Dados" icon="database" href="/l/pt/developers/extend/apps/data/overview">
Objetos, campos e relações bidirecionais.
</Card>
<Card title="Lógica" icon="bolt" href="/l/pt/developers/extend/apps/logic/overview">
Funções lógicas, habilidades, agentes e conexões OAuth.
</Card>
<Card title="Layout" icon="table-columns" href="/l/pt/developers/extend/apps/layout/overview">
Vistas, navegação, layouts de página, componentes de front-end.
</Card>
<Card title="Operações" icon="rocket" href="/l/pt/developers/extend/apps/operations/overview">
CLI, testes, remotos, CI e publicação do seu aplicativo.
</Card>
</CardGroup>

View File

@@ -0,0 +1,87 @@
---
title: Servidor Local
description: Gerencie o servidor Docker local do Twenty — iniciar, parar, atualizar, executar uma instância de teste paralela e configurar manualmente o SDK.
icon: server
---
## Gerenciando o servidor local
Use `yarn twenty docker:*` para controlar o contêiner Twenty local:
| Comando | O que faz |
| -------------------------------------- | ------------------------------------------------ |
| `yarn twenty docker:start` | Inicia o servidor (baixa a imagem se necessário) |
| `yarn twenty docker:start 2.2.0` | Iniciar uma versão específica do servidor |
| `yarn twenty docker:start --port 3030` | Inicia em uma porta personalizada |
| `yarn twenty docker:stop` | Interrompe o servidor (preserva os dados) |
| `yarn twenty docker:status` | Mostra a URL, a versão e as credenciais de login |
| `yarn twenty docker:logs` | Transmite os logs do servidor |
| `yarn twenty docker:reset` | Apaga os dados e começa do zero |
| `yarn twenty docker:upgrade` | Baixa a imagem mais recente `twenty-app-dev` |
| `yarn twenty docker:upgrade 2.2.0` | Atualiza para uma versão específica |
Os dados são persistidos entre reinicializações em dois volumes do Docker (`twenty-app-dev-data` para PostgreSQL, `twenty-app-dev-storage` para arquivos). Use `reset` para apagar tudo.
## Fixando a versão do servidor
Quando nenhuma versão é passada, `docker:start` resolve a versão a partir do intervalo `engines.twenty` do seu app em `package.json` — o mesmo intervalo que o servidor valida quando o seu app é instalado. Ele inicia a imagem mais recente publicada de `twenty-app-dev` que satisfaz o intervalo, recorrendo a `latest` quando o campo está ausente ou nenhuma versão publicada corresponde:
```json filename="package.json"
{
"engines": {
"twenty": ">=2.2.0"
}
}
```
Passe uma versão explicitamente para substituir o intervalo em uma única execução: `yarn twenty docker:start 2.3.0`. Se já existir um contêiner em uma versão diferente, `docker:start` faz o upgrade dele no lugar (recriando o contêiner enquanto preserva seus volumes de dados).
## Atualizando a imagem do servidor
`yarn twenty docker:upgrade` baixa a imagem mais recente, compara os digests e só recria o contêiner se algo realmente tiver mudado. Os volumes são preservados — apenas o contêiner é substituído. Se uma nova imagem foi baixada e o contêiner estava em execução, a atualização inicia automaticamente um novo contêiner; execute `yarn twenty docker:start` depois para aguardar até que ele fique saudável.
```bash filename="Terminal"
yarn twenty docker:upgrade # Latest
yarn twenty docker:upgrade 2.2.0 # Specific version
```
Verifique a versão em execução com `yarn twenty docker:status` (ele mostra o `APP_VERSION` incorporado ao contêiner).
## Executando uma instância de teste paralela
Passe `--test` para qualquer comando `docker:*` para gerenciar uma segunda instância totalmente isolada — útil para testes de integração ou para experimentar sem tocar nos seus dados principais de desenvolvimento:
| Comando | O que faz |
| ----------------------------------- | ------------------------------------------------ |
| `yarn twenty docker:start --test` | Inicia a instância de teste (padrão: porta 2021) |
| `yarn twenty docker:stop --test` | Para |
| `yarn twenty docker:status --test` | Mostra seu status |
| `yarn twenty docker:logs --test` | Transmite seus logs |
| `yarn twenty docker:reset --test` | Apaga seus dados |
| `yarn twenty docker:upgrade --test` | Atualiza sua imagem |
A instância de teste tem seu próprio contêiner (`twenty-app-dev-test`), volumes (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`) e configuração — ela é executada junto com sua instância principal sem conflitos. Combine `--test` com `--port` para substituir 2021.
## Configuração manual (sem o gerador)
Ignore o gerador se você estiver adicionando o SDK a um projeto existente:
```bash filename="Terminal"
yarn add twenty-sdk twenty-client-sdk
```
Adicione o script ao `package.json`:
```json filename="package.json"
{
"scripts": {
"twenty": "twenty"
}
}
```
Agora você pode executar `yarn twenty dev`, `yarn twenty docker:start` e o restante.
<Note>
Não instale `twenty-sdk` globalmente — fixe-o por projeto, para que cada aplicativo use sua própria versão.
</Note>

View File

@@ -0,0 +1,79 @@
---
title: Estrutura do projeto
description: O que há dentro de um app Twenty criado com scaffold — arquivos, pastas e o que cada um faz.
icon: folder-tree
---
Um novo app gerado por `npx create-twenty-app` se parece com isto:
```text filename="my-twenty-app/"
my-twenty-app/
package.json
src/
application-config.ts # Required — your app's entry point
default-role.ts # Permissions for logic functions
constants/
universal-identifiers.ts # Auto-generated UUIDs and metadata
front-components/
main-page.tsx # Welcome page component
navigation-menu-items/
main-page.navigation-menu-item.ts # Sidebar entry for the welcome page
page-layouts/
main-page.page-layout.ts # Standalone page hosting the component
__tests__/
application-config.test.ts # Unit test
global-setup.ts # Integration test setup (sync + uninstall)
schema.integration-test.ts # Integration test against a live server
.github/workflows/
ci.yml # Lint, typecheck, unit + integration tests
cd.yml # Deploy + install on push to main
publish.yml # Publish to npm on version tags (with provenance)
public/
logo.svg # Static assets
vitest.config.ts # Integration test runner config
vitest.unit.config.ts # Unit test runner config
tsconfig.json, tsconfig.spec.json
.nvmrc, .yarnrc.yml, .oxlintrc.json
README.md, AGENTS.md, CLAUDE.md, CHANGELOG.md, SETUP.md
```
## Arquivos principais
| Arquivo / Pasta | Finalidade |
| -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `src/application-config.ts` | **Obrigatório.** O principal arquivo de configuração do seu aplicativo. |
| `src/default-role.ts` | Papel padrão que controla o que suas funções de lógica podem acessar. |
| `src/constants/universal-identifiers.ts` | UUIDs gerados automaticamente e metadados (nome de exibição, descrição). |
| `src/front-components/`, `src/navigation-menu-items/`, `src/page-layouts/` | Uma página de boas-vindas inicial: um front component renderizado por um page layout autônomo, acessível a partir da barra lateral. |
| `src/__tests__/` | Um teste de unidade mais um teste de integração (com sua configuração global) que sincroniza o app com um servidor real. |
| `public/` | Recursos estáticos (imagens, fontes) servidos com seu aplicativo. |
| `AGENTS.md` / `CLAUDE.md` | Orientação para agentes de codificação de IA que trabalham no app. |
| `CHANGELOG.md` / `SETUP.md` | Registro de alterações importantes e instruções de configuração para desenvolvimento local. |
<Note>
**A organização de arquivos fica a seu critério.** As pastas acima são convenções — o SDK detecta entidades por meio de análise de AST em chamadas a `export default defineEntity(...)`, independentemente de onde o arquivo esteja.
</Note>
## Dependências
Ambos os pacotes Twenty SDK pertencem a `devDependencies`, não a `dependencies`:
```json filename="package.json"
{
"dependencies": {},
"devDependencies": {
"twenty-client-sdk": "2.20.0",
"twenty-sdk": "2.20.0",
"twenty-ui": "1.0.0-alpha.1"
}
}
```
O scaffolder fixa `twenty-sdk` e `twenty-client-sdk` para a sua própria versão — mantenha os dois sincronizados ao atualizar.
* **`twenty-sdk`** inclui a CLI `twenty` e as ferramentas de build/scaffolding. Ele é executado apenas durante o desenvolvimento e o build e nunca é importado pelo runtime do aplicativo publicado.
* **`twenty-client-sdk`** *é* importado pelo código do seu aplicativo (`CoreApiClient`, `MetadataApiClient`, `RestApiClient`), mas a Twenty o fornece em tempo de execução — as funções de lógica o obtêm de uma camada SDK gerada, e os componentes de front o resolvem a partir de módulos servidos pelo servidor. A cópia instalada é usada apenas para verificação de tipos e para o build no momento do deploy, então ela nunca precisa ser incluída no bundle implantado.
Manter qualquer um dos pacotes em `dependencies` o inclui no bundle de runtime do aplicativo instalado, onde ele é peso morto. `twenty dev:build` emite um aviso quando qualquer um deles ainda está listado em `dependencies`.
Adicione as dependências de runtime do próprio aplicativo (bibliotecas que as suas funções de lógica realmente importam em tempo de execução) em `dependencies`, como de costume. Mantenha a árvore enxuta: `dependencies` são instaladas em uma camada de runtime limitada a 250MB descompactados, e um comando de sync ou install falha com um erro de tamanho de dependencies acima desse valor — bibliotecas de UI e ferramentas de desenvolvimento pertencem a `devDependencies`.

View File

@@ -0,0 +1,166 @@
---
title: Início rápido
icon: rocket
description: Crie seu primeiro app do Twenty em minutos.
---
## Pré-requisitos
* **Node.js 24.5+** — [Baixar](https://nodejs.org/)
* **Yarn 4** — Vem com o Node.js via Corepack. Ative-o: `corepack enable`
* **Docker** — [Baixar](https://www.docker.com/products/docker-desktop/). Necessário para executar um servidor Twenty local. Ignore se você já tiver o Twenty em execução em outro lugar.
A criação de um aplicativo Twenty tem três fases. A ferramenta de scaffolding as reúne em um único comando do fluxo ideal, mas cada fase é um conceito separado — quando algo falha, saber em que fase você está indica o que corrigir.
| Fase | O que você faz | Ferramenta | Resultado |
| --------------------------- | -------------------------------------------------- | ----------------------------------- | ------------------------------------- |
| **1. Criar scaffolding** | Gerar o código-fonte do aplicativo | `npx create-twenty-app` | Um projeto TypeScript em disco |
| **2. Executar um servidor** | Iniciar um servidor Twenty para o qual sincronizar | Docker + `yarn twenty docker:start` | Uma instância Twenty em execução |
| **3. Sincronizar** | Sincronize seu código em tempo real com o servidor | `yarn twenty dev` | Suas alterações aparecem na interface |
---
## Fase 1 — Fazer scaffolding do seu projeto
Crie um novo aplicativo a partir do modelo:
```bash filename="Terminal"
npx create-twenty-app@latest my-twenty-app
```
O gerador não é interativo: o nome do diretório se torna o nome do app. Passe `--display-name` e `--description` para personalizar os metadados gerados (você também pode editá-los depois em `src/constants/universal-identifiers.ts`). Isso gera um projeto TypeScript em `my-twenty-app/` com um `application-config.ts` inicial, um papel padrão, fluxos de trabalho de CI/CD e um teste de integração.
**Após esta fase:** você tem o código-fonte de um aplicativo na sua máquina. Ele ainda não está em execução — isso é a Fase 2.
---
## Fase 2 — Executar um servidor Twenty local
Seu aplicativo precisa de um servidor Twenty para o qual sincronizar. O servidor é uma instância completa do Twenty — interface, API GraphQL, PostgreSQL — executando localmente no Docker. Seu código local envia suas definições para esse servidor, o que faz com que elas apareçam na interface.
O scaffolder inicia uma instância para você: com o Docker em execução, ele baixa a imagem `twentycrm/twenty-app-dev`, inicia-a na porta `2020` e autentica a CLI no workspace de demonstração pré-preenchido (`tim@apple.dev`) — sem necessidade de login.
Para se conectar a um servidor Twenty existente em vez disso, passe `--url \<your-server-url>`. Servidores remotos se autenticam com OAuth: um navegador é aberto para que você faça login e clique em **Authorize**, o que dá à CLI acesso ao seu workspace. (Você também pode optar por usar OAuth localmente com `--authentication-method oauth` — faça login com `tim@apple.dev` / `tim@apple.dev`.)
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/login.png" alt="Tela de login do Twenty" />
</div>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/authorize.png" alt="Tela de autorização da CLI do Twenty" />
</div>
Seu terminal confirmará que tudo está configurado.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/scaffolded.png" alt="Scaffold do aplicativo criado com sucesso" />
</div>
**Após esta fase:** você tem um servidor Twenty em execução em [http://localhost:2020](http://localhost:2020) com sua CLI autorizada a sincronizar com ele.
<Note>
Se o Docker não estiver instalado ou em execução, a ferramenta de scaffolding informará o comando de inicialização correto para o seu sistema operacional. Quando o Docker estiver ativo, você pode retomar com `yarn twenty docker:start` — sem necessidade de recriar o scaffolding.
</Note>
---
## Fase 3 — Sincronizar suas alterações
Este é o ciclo interno no qual você passará a maior parte do tempo.
```bash filename="Terminal"
cd my-twenty-app
yarn twenty dev
```
Isso monitora `src/`, recompila a cada alteração e sincroniza o resultado com o servidor. Edite um arquivo, salve e, em alguns segundos, o servidor refletirá a alteração. Você verá um painel de status em tempo real no seu terminal.
Para uma saída mais detalhada (logs de build, solicitações de sincronização, rastros de erro), adicione `--verbose`.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/dev.png" alt="Saída do terminal no modo de desenvolvimento" />
</div>
Abra [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer). Você deverá ver seu aplicativo em **Your Apps**.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-1.png" alt="Lista Your Apps exibindo My twenty app" />
</div>
Clique em **My twenty app** para ver seu **registro do aplicativo** — um registro em nível de servidor que descreve seu aplicativo (nome, identificador, credenciais OAuth, origem). Um registro pode ser instalado em vários espaços de trabalho no mesmo servidor.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-2.png" alt="Detalhes do registro do aplicativo" />
</div>
Clique em **View installed app** para ver a instalação no espaço de trabalho. A aba **About** mostra a versão e as opções de gerenciamento.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Aplicação instalada" />
</div>
**Após esta fase:** você tem um ciclo de desenvolvimento em tempo real. Edite qualquer arquivo em `src/` e ele aparecerá na interface.
### Sincronização única para CI e scripts
Use `plan` e `apply` para executar o mesmo pipeline uma vez, sem watcher:
```bash filename="Terminal"
yarn twenty plan # preview the metadata changes without applying them
yarn twenty apply # show the plan, then apply it
```
| Comando | Comportamento | Quando usar |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| `yarn twenty dev` | Monitora e ressincroniza a cada alteração. Fica em execução até você interrompê-lo. | Desenvolvimento local interativo. |
| `yarn twenty apply` | Executa uma única compilação + sincronização e, em seguida, encerra com o código `0` em caso de sucesso ou `1` em caso de falha. Pede confirmação para alterações destrutivas (passe `--force` para pular). | Scripts, CI, hooks de pre-commit, agentes de IA e fluxos de trabalho com script. |
| `yarn twenty plan` | Compila e imprime as alterações de metadados **sem aplicá-las**. | Inspecionar o que uma sincronização mudaria antes de confirmá-la. |
Todos os modos precisam de um remoto autenticado. Veja [Sincronização e recuperação](/l/pt/developers/extend/apps/operations/sync-and-recovery#previewing-changes-plan) para mais detalhes sobre `plan`.
<Note>
`yarn twenty dev --once` e `yarn twenty dev --once --dry-run` são aliases obsoletos para `yarn twenty apply` e `yarn twenty plan`.
</Note>
### Opções do modo de desenvolvimento
| Opção | Descrição |
| ------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `--force` | Aplicar alterações destrutivas (exclusões) sem confirmação. |
| `--debounceMs \<ms>` | Define o atraso de debounce para alterações de arquivo em milissegundos (padrão: `1000`). |
| `--verbose` / `--debug` | Mostra registros detalhados de compilação, solicitações de sincronização e rastreamentos de erro. |
## O que você pode criar
Os aplicativos são compostos por **entidades** — cada uma definida como um arquivo TypeScript com um único `export default`:
| Entidade | O que faz |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| **Objetos e campos** | Modelos de dados personalizados (Cartão postal, Fatura etc.) com campos tipados |
| **Funções lógicas** | Funções TypeScript do lado do servidor acionadas por rotas HTTP, agendamentos do cron ou eventos de banco de dados |
| **Componentes de front-end** | Componentes React que são renderizados na UI do Twenty (painel lateral, widgets, menu de comandos) |
| **Habilidades e agentes** | Recursos de IA — instruções reutilizáveis e assistentes autônomos |
| **Exibições e navegação** | Exibições de lista pré-configuradas e itens de menu da barra lateral |
| **Layouts de página** | Páginas de detalhes de registros personalizadas com abas e widgets |
Referência completa: [Conceitos](/l/pt/developers/extend/apps/getting-started/concepts).
## Próximos passos
<CardGroup cols={2}>
<Card title="Configuração" icon="screwdriver-wrench" href="/l/pt/developers/extend/apps/config/overview">
Identidade do aplicativo, função padrão, hooks de instalação e desinstalação, recursos públicos.
</Card>
<Card title="Data" icon="database" href="/l/pt/developers/extend/apps/data/overview">
Objetos, campos e relações bidirecionais.
</Card>
<Card title="Lógica" icon="bolt" href="/l/pt/developers/extend/apps/logic/overview">
Funções lógicas, habilidades, agentes e conexões OAuth.
</Card>
<Card title="Layout" icon="table-columns" href="/l/pt/developers/extend/apps/layout/overview">
Exibições, navegação, layouts de página, componentes de front-end.
</Card>
<Card title="Operações" icon="rocket" href="/l/pt/developers/extend/apps/operations/overview">
CLI, testes, remotos, CI e publicação do seu aplicativo.
</Card>
</CardGroup>

View File

@@ -0,0 +1,63 @@
---
title: Scaffolding
description: Gere arquivos de entidade de forma interativa com `yarn twenty dev:add` — objetos, campos, visualizações, funções de lógica e mais.
icon: wand-magic-sparkles
---
Em vez de criar arquivos de entidade manualmente, use o scaffolder interativo:
```bash filename="Terminal"
yarn twenty dev:add
```
Ele solicita que você escolha um tipo de entidade e orienta você pelos campos obrigatórios, depois grava 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:
```bash filename="Terminal"
yarn twenty dev:add object
yarn twenty dev:add logicFunction
yarn twenty dev:add frontComponent
```
## Tipos de entidade disponíveis
| Tipo de entidade | Comando | Arquivo gerado |
| ------------------------------- | ------------------------------------------ | --------------------------------------------------------- |
| Objeto | `yarn twenty dev:add object` | `src/objects/\<name>.ts` |
| Campo | `yarn twenty dev:add field` | `src/fields/\<name>.ts` |
| Função lógica | `yarn twenty dev:add logicFunction` | `src/logic-functions/\<name>.ts` |
| Componente de front-end | `yarn twenty dev:add frontComponent` | `src/front-components/\<name>.tsx` |
| Função | `yarn twenty dev:add role` | `src/roles/\<name>.ts` |
| Habilidade | `yarn twenty dev:add skill` | `src/skills/\<name>.ts` |
| Agente | `yarn twenty dev:add agent` | `src/agents/\<name>.ts` |
| Vista | `yarn twenty dev:add view` | `src/views/\<name>.ts` |
| Item do menu de navegação | `yarn twenty dev:add navigationMenuItem` | `src/navigation-menu-items/\<name>.ts` |
| Layout da página | `yarn twenty dev:add pageLayout` | `src/page-layouts/\<name>.ts` |
| Aba Layout da Página | `yarn twenty dev:add pageLayoutTab` | `src/page-layout-tabs/\<name>.ts` |
| Item do menu de comandos | `yarn twenty dev:add commandMenuItem` | `src/command-menu-items/\<name>.ts` |
| Campo da Vista | `yarn twenty dev:add viewField` | `src/view-fields/\<name>.ts` |
| Provedor de conexão | `yarn twenty dev:add connectionProvider` | `src/connection-providers/\<name>.ts` |
| Tipo de atividade da cronologia | `yarn twenty dev:add timelineActivityType` | `src/timeline-activity-types/\<name>.ts` |
## O que o scaffolder gera
Cada tipo de entidade tem seu próprio modelo. Por exemplo, `yarn twenty dev: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:
```bash filename="Terminal"
yarn twenty dev:add logicFunction --path src/custom-folder
```

View File

@@ -0,0 +1,14 @@
---
title: Resolução de Problemas
description: Problemas comuns na primeira execução — Docker, versão do Node, Yarn, dependências.
icon: wrench
---
* **Erros do Docker** — Certifique-se de que o Docker Desktop (ou o daemon) esteja em execução antes de `yarn twenty docker:start`. A mensagem de erro mostrará o comando de inicialização correto para o seu sistema operacional.
* **Versão errada do Node** — É necessário 24.5+ (`engines.node: ^24.5.0`). Verifique com `node -v`.
* **Falta o Yarn 4** — Execute `corepack enable`.
* **Dependências com problemas** — `rm -rf node_modules && yarn install`.
* **Erros do `twenty-sdk` após a atualização para a v2.8.0** — Ele foi movido de `dependencies` para `devDependencies` na v2.8.0. Veja [Estrutura do projeto → Dependências](/l/pt/developers/extend/apps/getting-started/project-structure#dependencies).
* **`twenty dev:build` emite um aviso sobre `twenty-client-sdk` em `dependencies`** — Ele é fornecido em tempo de execução pela Twenty, então deve ser movido para `devDependencies` junto com `twenty-sdk`. Veja [Estrutura do projeto → Dependências](/l/pt/developers/extend/apps/getting-started/project-structure#dependencies).
Travou? Peça ajuda no [Discord da Twenty](https://discord.com/channels/1130383047699738754/1130386664812982322).

View File

@@ -0,0 +1,118 @@
---
title: Itens do menu de comandos
description: Exponha componentes de front-end como ações rápidas e entradas do menu de comandos (Cmd+K) com `defineCommandMenuItem`.
icon: terminal
---
Um **item de menu de comando** é a ponte entre o usuário e um [front component](/l/pt/developers/extend/apps/layout/front-components). Ele registra o componente no menu de comandos (Cmd+K) do Twenty e, opcionalmente, como um botão fixado de ação rápida no canto superior direito da página.
```ts src/command-menu-items/open-dashboard.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
label: 'Open Dashboard',
shortLabel: 'Dashboard',
isPinned: true,
availabilityType: 'GLOBAL',
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
});
```
## Campos de configuração
| Campo | Obrigatório | Descrição |
| --------------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `universalIdentifier` | Sim | ID exclusivo e estável para o comando |
| `label` | Sim | Rótulo completo exibido no menu de comandos (Cmd+K) |
| `frontComponentUniversalIdentifier` | Sim | O `universalIdentifier` do componente de front-end que este comando abre |
| `shortLabel` | Não | Rótulo mais curto exibido no botão fixado de ação rápida |
| `icon` | Não | **Obsoleto** — ignorado em favor do ícone da aplicação; a compilação emite um aviso se definido |
| `isPinned` | Não | Quando `true`, mostra o comando como um botão de ação rápida no canto superior direito da página |
| `availabilityType` | Não | Controla onde o comando aparece: `'GLOBAL'` (sempre disponível), `'GLOBAL_OBJECT_CONTEXT'` (apenas em páginas com um contexto de objeto — páginas de índice e de registro), `'RECORD_SELECTION'` (apenas quando registros estão selecionados) ou `'FALLBACK'` (exibido quando nenhum outro comando corresponde) |
| `availabilityObjectUniversalIdentifier` | Não | Restringe o comando a páginas de um tipo específico de objeto (por exemplo, somente em registros de Company) |
| `conditionalAvailabilityExpression` | Não | Uma expressão booleana que controla dinamicamente a visibilidade (veja abaixo) |
## Comandos sem interface
Um item do menu de comandos emparelhado com um [componente de front-end sem interface](/l/pt/developers/extend/apps/layout/front-components#headless-vs-non-headless) é a forma idiomática de disponibilizar uma ação de um clique — executar código, navegar ou confirmar e executar. A página de Front Components aborda os [SDK Command components](/l/pt/developers/extend/apps/layout/front-components#sdk-command-components) (`Command`, `CommandLink`, `CommandModal`, `CommandOpenSidePanelPage`) que lidam com o padrão de ação e desmontagem.
Um fluxo típico: um componente headless renderiza `<Command execute={...} />` (veja o [exemplo completo](/l/pt/developers/extend/apps/layout/front-components#sdk-command-components)), e o item de menu de comando aponta para ele:
```ts src/command-menu-items/run-action.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
});
```
## 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:
```ts src/command-menu-items/bulk-update.command-menu-item.ts
import {
defineCommandMenuItem,
objectPermissions,
everyEquals,
} from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '...',
label: 'Bulk Update',
availabilityType: 'RECORD_SELECTION',
frontComponentUniversalIdentifier: '...',
conditionalAvailabilityExpression: everyEquals(
objectPermissions,
'canUpdateObjectRecords',
true,
),
});
```
<Note>
`RECORD_SELECTION` já implica uma seleção não vazia — use `numberOfSelectedRecords` apenas para contagens específicas (por exemplo, `>= 2`).
</Note>
### Variáveis de contexto
Elas representam o estado atual da página:
| Variável | Tipo | Descrição |
| ------------------------------ | --------- | --------------------------------------------------------------------------- |
| `pageType` | `string` | Tipo de página atual (por exemplo, `'RecordIndexPage'`, `'RecordShowPage'`) |
| `isInSidePanel` | `boolean` | Se o componente é renderizado em um painel lateral |
| `numberOfSelectedRecords` | `number` | Número de registros atualmente selecionados |
| `isSelectAll` | `boolean` | Se "selecionar tudo" está ativo |
| `selectedRecords` | `array` | Os objetos de registro selecionados |
| `favoriteRecordIds` | `array` | IDs dos registros marcados como favoritos |
| `objectPermissions` | `object` | Permissões para o tipo de objeto atual |
| `targetObjectReadPermissions` | `object` | Permissões de leitura para o objeto alvo |
| `targetObjectWritePermissions` | `object` | Permissões de escrita para o objeto alvo |
| `featureFlags` | `object` | Flags de recurso ativas |
| `objectMetadataItem` | `object` | Metadados do tipo de objeto atual |
| `hasAnySoftDeleteFilterOnView` | `boolean` | Se a visualização atual tem um filtro de soft-delete |
### Operadores
Combine variáveis em expressões booleanas:
| Operador | Descriçã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 |

View File

@@ -0,0 +1,849 @@
---
title: Componentes de front-end
description: Crie componentes React que renderizam dentro da UI do Twenty com isolamento em sandbox.
icon: window-maximize
---
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 é executado dentro de um iframe de origem opaca e em sandbox, mas sua interface ainda é renderizada de forma nativa na página em vez de ficar confinada a esse iframe.
<Warning>
Os componentes da frente ainda estão em desenvolvimento activo. Seu código roda contra um DOM parcial, não uma página real do navegador, então usos avançados podem falhar, geralmente silenciosamente. Veja [Limitação atual](#current-limitations).
</Warning>
## Onde os componentes de front-end podem ser usados
Os componentes de front-end podem ser renderizados em três 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 dentro de [layouts de página](/l/pt/developers/extend/apps/layout/page-layouts). 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.
* **Configurações do aplicativo** — Definido com [`defineSettingsFrontComponent()`](#custom-settings-component), o componente de front-end é renderizado como uma seção dentro da aba **Settings** do aplicativo, no lugar da interface padrão de configuração de variáveis.
Um componente de front-end por si só não é acessível pela UI — é preciso *exibi-lo*. As três maneiras de fazer isso são:
* **Associe-o a um [item do menu de comandos](/l/pt/developers/extend/apps/layout/command-menu-items)** — registra-o no menu de comandos (Cmd+K) e, opcionalmente, como uma ação rápida fixada.
* **Incorpore-o como um widget em um [layout de página](/l/pt/developers/extend/apps/layout/page-layouts)** — posiciona-o na página de detalhes de um registro ou em um painel.
* **Definindo-o com [`defineSettingsFrontComponent()`](#custom-settings-component)** — o componente é renderizado como uma seção dentro da aba **Settings** do aplicativo, no lugar da interface padrão de configuração de variáveis.
## Exemplo básico
A maneira mais rápida de ver um componente de front-end em ação é associá-lo a um [`defineCommandMenuItem`](/l/pt/developers/extend/apps/layout/command-menu-items), para que ele apareça como um botão de ação rápida no canto superior direito da página:
```tsx src/front-components/hello-world.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
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,
});
```
```ts src/command-menu-items/hello-world.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
shortLabel: 'Hello',
label: 'Hello World',
isPinned: true,
availabilityType: 'GLOBAL',
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
});
```
Após sincronizar com `yarn twenty dev` (ou executando uma única vez o `yarn twenty apply`), a ação rápida aparece no canto superior direito da página:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="Botão de ação rápida no canto superior direito" />
</div>
Clique nele para renderizar o componente inline.
## Campos de configuração
| Campo | Obrigatório | Descrição |
| --------------------- | ----------- | ---------------------------------------------------------------------------- |
| `universalIdentifier` | sim | ID único e estável para este componente |
| `component` | Sim | Uma função de componente React |
| `name` | Não | Nome de Exibição |
| `description` | Não | Descrição do que o componente faz |
| `isHeadless` | Não | Defina como `true` se o componente não tiver interface visível (veja 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 [Layouts de página](/l/pt/developers/extend/apps/layout/page-layouts) para detalhes.
## Componente de configurações personalizadas
Para substituir a interface de configuração de variáveis gerada automaticamente na aba **Settings** do seu aplicativo pelo seu próprio componente, defina-o com `defineSettingsFrontComponent` em vez de `defineFrontComponent`. Ele usa os mesmos [campos de configuração](#configuration-fields) (exceto `isHeadless`, que não é aceito, já que um componente de configurações sempre renderiza uma interface visível) e, adicionalmente, marca o componente como a interface de configurações do app.
O componente é renderizado como uma seção **dentro** da aba Settings, e não como uma substituição de toda a aba. As seções gerenciadas pelo sistema do Twenty — atualização automática, App URL e conexões — são sempre renderizadas acima dela e não podem ser substituídas pelo app.
```tsx src/front-components/app-settings.tsx
import { defineSettingsFrontComponent } from 'twenty-sdk/define';
const AppSettings = () => {
return (
<div style={{ padding: '20px' }}>
<h2>My app settings</h2>
{/* render your own configuration UI here */}
</div>
);
};
export default defineSettingsFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'app-settings',
description: "Custom UI for the app's Settings tab",
component: AppSettings,
});
```
Apenas um componente de configurações de front-end é permitido por app; declarar mais de um faz com que a build falhe. Quando presente, a aba **Settings** do app renderiza este componente no lugar da interface padrão de configuração de variáveis.
## 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`)** — O componente é montado de forma invisível em segundo plano. 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.
```tsx src/front-components/sync-tracker.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds, enqueueSnackbar } from 'twenty-sdk/front-component';
import { useEffect } from 'react';
const SyncTracker = () => {
const [recordId] = useSelectedRecordIds();
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/front-component`:
* **`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 do painel lateral. As props dependem de `page` — por exemplo, `ViewRecord` recebe `recordId` + `objectNameSingular` (além de um id de `tab` opcional para abrir o registro em uma guia específica), outras páginas recebem `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:
```tsx src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Command } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
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,
});
```
```ts src/command-menu-items/run-action.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
});
```
E um exemplo usando `CommandModal` para solicitar confirmação antes de executar:
```tsx src/front-components/delete-draft.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { CommandModal } from 'twenty-sdk/front-component';
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,
});
```
E um exemplo usando `CommandOpenSidePanelPage` para abrir o registro atual no painel lateral em uma guia específica. `tab` é um id de guia de layout de página (layouts padrão usam ids como `company-tab-emails` ou `company-tab-timeline`; layouts personalizados usam o próprio id da guia). Se o id não existir no layout do registro, a guia padrão será aberta em seu lugar:
```tsx src/front-components/open-company-emails.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import {
CommandOpenSidePanelPage,
SidePanelPages,
useSelectedRecordIds,
} from 'twenty-sdk/front-component';
const OpenCompanyEmails = () => {
const selectedRecordIds = useSelectedRecordIds();
const recordId = selectedRecordIds.length === 1 ? selectedRecordIds[0] : null;
if (!recordId) {
return null;
}
return (
<CommandOpenSidePanelPage
page={SidePanelPages.ViewRecord}
recordId={recordId}
objectNameSingular="company"
tab="company-tab-emails"
resetNavigationStack={false}
/>
);
};
export default defineFrontComponent({
universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
name: 'open-company-emails',
description: 'Opens the current company on its Emails tab',
component: OpenCompanyEmails,
isHeadless: true,
});
```
## Chamando uma função lógica
Os componentes de front são executados no navegador em um Web Worker em sandbox dentro de um iframe de origem opaca, enquanto as [funções lógicas](/l/pt/developers/extend/apps/logic/logic-functions) são executadas no servidor. Não há chamada direta no mesmo processo entre os dois — em vez disso, um componente de front acessa uma função lógica via HTTP.
Uma função lógica declarada com `httpRouteTriggerSettings` é acessível por HTTP em seu caminho de rota. `RestApiClient` trata caminhos que começam com `/s/` como rotas de aplicativo, resolve-os para a URL a partir da qual suas funções são servidas e os autentica com `TWENTY_APP_ACCESS_TOKEN`.
> **No Twenty Cloud, funções lógicas acionadas por HTTP são servidas em um domínio dedicado por workspace** em `https://\<your-workspace-subdomain>.withtwenty.com\<path>`. Para chamadores externos, copie a URL exata das configurações de **HTTP trigger** da função ou da guia **Settings** do aplicativo.
Um componente de front headless pode executar a chamada ao montar via o componente `Command` e, em seguida, desmontar automaticamente:
```tsx src/front-components/sync-prs.tsx
import { RestApiClient } from 'twenty-client-sdk/rest';
import { defineFrontComponent } from 'twenty-sdk/define';
import { Command } from 'twenty-sdk/front-component';
const SyncPrs = () => {
const execute = async () => {
await new RestApiClient().post('/s/github/fetch-prs', {
owner: 'twentyhq',
repo: 'twenty',
});
};
return <Command execute={execute} />;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'sync-prs',
description: 'Triggers the fetch-prs logic function',
isHeadless: true,
component: SyncPrs,
});
```
O caminho passado para o `RestApiClient` é o `httpRouteTriggerSettings.path` da função de lógica, prefixado com `/s`. Mantenha `isAuthRequired: true`; o `TWENTY_APP_ACCESS_TOKEN` que a Twenty gera para o seu componente autentica a solicitação:
```ts src/logic-functions/fetch-prs.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import type { RoutePayload } from 'twenty-sdk/logic-function';
const handler = async (event: RoutePayload) => {
const { owner, repo } = (event.body ?? {}) as { owner: string; repo: string };
// ...fetch from GitHub and persist records...
return { ok: true };
};
export default defineLogicFunction({
universalIdentifier: '...',
name: 'fetch-prs',
handler,
httpRouteTriggerSettings: {
path: '/github/fetch-prs',
httpMethod: 'POST',
isAuthRequired: true,
},
});
```
<Note>
`TWENTY_APP_ACCESS_TOKEN` é injetado automaticamente — consulte [Variáveis de aplicação](#application-variables). Como as variáveis de aplicação secretas nunca são expostas aos componentes de front, mantenha as chaves de API e outra lógica sensível na função lógica, não no componente de front.
</Note>
### Chamando a API REST da Twenty
Para chamar rotas HTTP do aplicativo ou ler e gravar registros da Twenty a partir de um front component, use `RestApiClient` de `twenty-client-sdk/rest`. Ele envia caminhos `/s/...` para a URL base das funções do seu workspace e qualquer outro caminho, incluindo `/rest/...`, para `TWENTY_API_URL`.
Ele sempre atua como a pessoa que está visualizando a página. `runAs: 'application'` é uma opção apenas de função de lógica: um componente nunca recebe o token do seu próprio aplicativo, portanto, solicitá-lo aqui gera um erro. Coloque o trabalho que precisa do acesso do próprio aplicativo atrás de uma função de lógica e chame-a em vez disso.
| Método | Descrição |
| --------------------------------- | ------------------------------------------------------------------------------- |
| `get(path, options?)` | Envia uma requisição `GET` |
| `post(path, body?, options?)` | Envia uma requisição `POST` |
| `put(path, body?, options?)` | Envia uma requisição `PUT` |
| `patch(path, body?, options?)` | Envia uma requisição `PATCH` |
| `delete(path, options?)` | Envia uma requisição `DELETE` |
| `request(method, path, options?)` | Requisição genérica com qualquer método HTTP |
| `resolveUrl(path, options?)` | Resolve um caminho para sua URL completa sem enviar uma requisição (para links) |
`options` aceita `headers`, `query` (um registro de parâmetros de query string; valores nulos ou indefinidos são ignorados) e um `AbortSignal` via `signal`. Um objeto `body` que não seja `FormData` é serializado em JSON automaticamente. Em um `401`, o cliente atualiza o access token uma vez por meio do host e tenta a requisição novamente.
A URL base e o token são resolvidos do ambiente por padrão. Passe substituições (overrides) para o construtor quando necessário — por exemplo, em testes:
```ts
const client = new RestApiClient({
baseUrl: 'https://myworkspace.twenty.com',
token: 'my-token',
});
```
Requisições com falha geram um erro `RestApiClientError` que expõe `status`, `statusText`, `url` e o `body` analisado:
```tsx
import { RestApiClient, RestApiClientError } from 'twenty-client-sdk/rest';
const client = new RestApiClient();
try {
const people = await client.get('/rest/people', {
query: { limit: 10 },
});
} catch (error) {
if (error instanceof RestApiClientError) {
console.error(error.status, error.body);
}
}
```
## 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:
```tsx src/front-components/record-info.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import {
useUserId,
useSelectedRecordIds,
useFrontComponentId,
} from 'twenty-sdk/front-component';
const RecordInfo = () => {
const userId = useUserId();
const [recordId] = useSelectedRecordIds();
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:
| Hook | Retorna | Descrição |
| --------------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------- |
| `useUserId()` | `string` ou `null` | O ID do usuário atual |
| `useSelectedRecordIds()` | `string[]` | Todos os IDs dos registros selecionados (array vazio se nenhum estiver selecionado) |
| `useRecordId()` | `string` ou `null` | **Obsoleto.** Use `useSelectedRecordIds()` em vez disso |
| `useFrontComponentId()` | `string` | O ID desta instância do componente |
| `useTimelineActivityId()` | `string` ou `null` | O ID da atividade atual da linha do tempo ao renderizar uma linha personalizada da linha do tempo |
| `useColorScheme()` | `'light'` ou `'dark'` | O esquema de cores ativo da interface do host (`System` já está resolvido) |
| `useFrontComponentExecutionContext(selector)` | varia | Acesse o contexto de execução completo com uma função seletora |
## Variáveis de aplicação
Variáveis de aplicação definidas em [`defineApplication()`](/l/pt/developers/extend/apps/config/application) com `isSecret: false` estão disponíveis nos componentes de front por meio do utilitário `getApplicationVariable`:
```tsx src/front-components/greeting.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { getApplicationVariable } from 'twenty-sdk/front-component';
const Greeting = () => {
const recipientName = getApplicationVariable('DEFAULT_RECIPIENT_NAME') ?? 'World';
return <p>Hello, {recipientName}!</p>;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'greeting',
component: Greeting,
});
```
<Warning>
Variáveis secretas (`isSecret: true`) **não** são expostas aos componentes de front. Elas estão disponíveis apenas em [funções de lógica](/l/pt/developers/extend/apps/logic/logic-functions), que são executadas no lado do servidor. Isso impede que valores sigilosos, como chaves de API, sejam enviados para o navegador.
</Warning>
`getApplicationVariable` sempre retorna uma **string** (ou `undefined`), independentemente do `type` declarado da variável. A string é serializada de forma consistente por tipo (booleanos como `"true"` / `"false"`, números como strings decimais, arrays / objetos como JSON), o mesmo formato usado para a logic-function `process.env` — faça você mesmo o parse (`Number(...)`, `JSON.parse(...)`, `=== 'true'`). Veja [Tipos de variáveis](/l/pt/developers/extend/apps/config/application#variable-types).
As seguintes variáveis de sistema estão sempre disponíveis via `process.env`:
| Variável | Descrição |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TWENTY_API_URL` | URL base da API principal da Twenty |
| `TWENTY_APP_ACCESS_TOKEN` | Token de curta duração com escopo limitado à interseção entre a função da pessoa conectada e a do seu aplicativo, de modo que um componente nunca possa fazer mais do que a pessoa que o está visualizando |
### `TWENTY_FUNCTIONS_URL`
A Twenty também injeta `TWENTY_FUNCTIONS_URL` em front components e funções de lógica: a URL base a partir da qual as funções de lógica acionadas por HTTP do seu aplicativo são servidas.
Ela existe porque essa URL nem sempre é o próprio servidor da Twenty. No Twenty Cloud, as rotas do aplicativo são servidas em um domínio dedicado por workspace (`https://\<your-workspace-subdomain>.withtwenty.com`, ou o domínio público primário da aplicação quando um é configurado) para que respostas criadas pelo aplicativo sejam executadas em uma origem isolada, em vez de na origem do aplicativo Twenty. Instâncias self-hosted e locais servem rotas do aplicativo sob o prefixo `/s` no próprio servidor e podem não definir a variável. Como a URL base varia por workspace e por instância, seu código não pode defini-la de forma fixa — o servidor injeta o valor correto em tempo de execução.
Você raramente precisa lê-la diretamente. Chame suas rotas por meio de `RestApiClient` com um caminho prefixado com `/s/` e o cliente resolverá a URL para você: ele remove o prefixo `/s` e direciona para `TWENTY_FUNCTIONS_URL`, recorrendo a `\<TWENTY_API_URL>/s` quando a variável não está definida. Use `resolveUrl('/s/\<path>')` para obter a URL absoluta sem enviar uma requisição, por exemplo, para um link. Leia a variável diretamente apenas ao construir uma URL manualmente:
```ts
const routeUrl = `${process.env.TWENTY_FUNCTIONS_URL || `${process.env.TWENTY_API_URL}/s`}/documents/generate`;
```
## 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ção | Descrição |
| -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `navigate(to, params?, queryParams?, options?)` | Navegar para uma página no app |
| `openSidePanelPage(params)` | Abrir um painel lateral |
| `closeSidePanel()` | Fechar 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 |
| `uploadFile(file, { fieldMetadataId, fileName? })` | Carrega um `Blob` em um campo FILES; resulta em `{ status: 'uploaded', file: { fileId, path, url, size, mimeType } }` ou `{ status: 'failed', reason: 'invalid-params' \| 'upload-failed' }` |
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:
```tsx src/front-components/archive-record.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { enqueueSnackbar, closeSidePanel, useSelectedRecordIds } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
const ArchiveRecord = () => {
const [recordId] = useSelectedRecordIds();
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,
});
```
### Armazenamento
`localStorage` e `sessionStorage` funcionam como em uma página normal, com a API síncrona padrão. Suas chaves são limitadas à instalação do seu app e ao usuário conectado: nenhum outro app pode lê-las, e outro usuário que entrar no mesmo navegador começará com um armazenamento vazio. Os valores gravados em `localStorage` permanecem no dispositivo entre recarregamentos; `sessionStorage` permanece durante a sessão do navegador.
```tsx src/front-components/note-draft.tsx
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
const NoteDraft = () => {
const [draft, setDraft] = useState(() => localStorage.getItem('draft') ?? '');
const handleChange = (nextDraft: string) => {
setDraft(nextDraft);
localStorage.setItem('draft', nextDraft);
};
return (
<textarea value={draft} onChange={(event) => handleChange(event.target.value)} />
);
};
export default defineFrontComponent({
universalIdentifier: 'd0e1f2a3-b4c5-6789-def0-890123456789',
name: 'note-draft',
description: 'Keeps an unsaved note on this device',
component: NoteDraft,
});
```
Twenty armazena os valores em nome do seu app, então uma gravação é aplicada localmente imediatamente e salva em segundo plano. Leituras nunca esperam pelo host. Nada é sincronizado: os valores não acompanham o usuário para outro navegador ou máquina, portanto use o [armazenamento de chave-valor](/l/pt/developers/extend/apps/logic/key-value-store) de uma função de lógica para qualquer coisa que precise sobreviver a uma troca de dispositivo.
As gravações têm limite, e cada limite contabiliza caracteres em vez de bytes: as chaves têm no máximo 512 caracteres, um único valor no máximo 262.144 caracteres e cada armazenamento no máximo 1.048.576 caracteres por app e usuário. Uma gravação que ultrapasse um limite gera um `QuotaExceededError`, como na API do navegador.
### Trabalhando com vários registros
Use `useSelectedRecordIds()` para lidar com vários registros selecionados. Isso é útil para operações em lote:
```tsx src/front-components/bulk-export.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
const BulkExport = () => {
const selectedRecordIds = useSelectedRecordIds();
const handleExport = async () => {
const client = new CoreApiClient();
for (const recordId of selectedRecordIds) {
await client.mutation({
updateTask: {
__args: { id: recordId, data: { exported: true } },
id: true,
},
});
}
await enqueueSnackbar({
message: `Exported ${selectedRecordIds.length} records`,
variant: 'success',
});
await closeSidePanel();
};
return (
<div style={{ padding: '20px' }}>
<p>Export {selectedRecordIds.length} selected record(s)?</p>
<button onClick={handleExport}>Export</button>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
name: 'bulk-export',
description: 'Export selected records',
component: BulkExport,
});
```
Exiba-o com um [item de menu de comando](/l/pt/developers/extend/apps/layout/command-menu-items) restrito a seleções de registros:
```ts src/command-menu-items/bulk-export.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902',
label: 'Bulk Export',
availabilityType: 'RECORD_SELECTION',
frontComponentUniversalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
});
```
## Recursos públicos
Componentes de front-end podem acessar arquivos do diretório `public/` do app usando `getPublicAssetUrl`:
```tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';
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](/l/pt/developers/extend/apps/config/public-assets) para obter detalhes.
## Compartilhar dependências pela frente de componentes
Por padrão, cada componente frontal junta sua própria cópia das bibliotecas que importa, então um aplicativo com cinco componentes é React cinco vezes. Declarar as dependências compartilhadas no `pacote` do seu aplicativo. son\` para construir essas bibliotecas uma vez e ter cada componente do aplicativo carregá-las de um único arquivo em cache:
```json package.json
{
"frontComponentSharedDependencies": ["react", "react-dom/client", "twenty-ui/input"]
}
```
Cada componente então importa suas dependências exatamente como antes — nada muda no seu código do componente:
```tsx src/front-components/counter.tsx
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
const Counter = () => {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'counter',
component: Counter,
});
```
Algumas coisas que você precisa saber:
* **Um pacote de dependências compartilhado por aplicativo.** O pacote é construído a partir das dependências do seu aplicativo, então você mantém controle total das versões que você enviou.
* **Liste os especificadores exatos que você importa.** `vinte e input` e `vinte e seis/display` são duas referências; um nome de pacote só por si não cobre seus subcaminhos. A listagem do `react` cobre automaticamente o `react/jsx-runtime`.
* **Compartilhe `react-dom/client` ao lado de `react`.** Cada componente renderiza através de `createRoot`, então deixar de fora significa que cada componente ainda empacota React DOM.
* \*\*O pacote é armazenado em cache. \* É servido sob uma URL de hash de conteúdo com um cache imutável de longa duração, portanto, ele é baixado uma vez e reutilizado em todos os componentes do aplicativo até que uma de suas dependências seja alterada.
* **Componentes que importam nenhum dos pacotes compartilhados nunca baixá-lo.**
## Estilização
Componentes de front-end suportam várias abordagens de estilização. Você pode usar:
* **Estilos inline** — `style={{ color: 'red' }}`
* **Componentes de UI da Twenty** — a própria biblioteca de componentes da Twenty; consulte [Usando componentes de UI da Twenty](#using-twenty-ui-components) abaixo
* **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
## Usando componentes de UI da Twenty
Twenty distribui sua biblioteca de componentes como o pacote [`twenty-ui`](https://www.npmjs.com/package/twenty-ui/v/1.0.0-alpha.1). Os componentes de front-end podem usá-lo para botões, tags, pílulas de status, chips, avatares, ícones, tipografia e tokens de tema que correspondem automaticamente ao tema claro e escuro do espaço de trabalho.
### Instalação
Adicione o pacote ao seu app, fixado na versão fornecida pela sua instância do Twenty:
```bash
yarn add twenty-ui@1.0.0-alpha.1
```
`twenty-ui` é empacotado no seu componente de front-end em tempo de build, então ele só precisa ser uma dependência do seu app — não há nada para configurar em tempo de execução.
### Importando componentes
Importe a partir do subcaminho correspondente em vez da raiz do pacote, para que apenas os componentes que você usa acabem no seu bundle:
| Subcaminho | O que ele exporta |
| --------------------------- | ----------------------------------------------- |
| `twenty-ui/input` | `Button` e inputs de formulário |
| `twenty-ui/data-display` | `Tag`, `Status`, `Chip`, `Avatar` e mais |
| `twenty-ui/feedback` | `Callout`, `Banner`, `Info` e mais |
| `twenty-ui/typography` | `H1Title`, `H2Title`, `H3Title`, `Label` e mais |
| `twenty-ui/icon` | Componentes `Icon*` (por exemplo, `IconCheck`) |
| `twenty-ui/theme-constants` | `ThemeProvider`, `themeCssVariables` |
```tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Status, Tag } from 'twenty-ui/data-display';
import { Button } from 'twenty-ui/input';
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,
});
```
### Ícones
Importe ícones individuais de `twenty-ui/icon`:
```tsx
import { IconBox, IconCheck } from 'twenty-ui/icon';
```
Cada ícone nomeado é tree-shaken, então importar alguns adiciona pouco ao seu bundle. Evite `IconsProvider`, `useIcons` e `iconsState` — eles trazem todo o conjunto de ícones Tabler (vários MB).
### Temas e tokens de tema
Os componentes do Twenty UI correspondem automaticamente ao tema claro e escuro do espaço de trabalho — o renderizador aplica o esquema de cores ativo no host, e os componentes resolvem suas cores com base nele.
Para usar os mesmos tokens de design nos seus próprios estilos inline, chame o hook `useTheme()`. Ele retorna os tokens de tema do Twenty (espaçamento, cores, raios, fontes) conectados ao tema ativo, sem necessidade de configurar `ThemeProvider` no seu componente:
```tsx
import { useTheme } from 'twenty-ui/theme-constants';
const Card = () => {
const theme = useTheme();
return (
<div
style={{
padding: theme.spacing[4],
background: theme.background.secondary,
color: theme.font.color.primary,
}}
>
Themed card
</div>
);
};
```
Como `useTheme()` é um hook, você lê os tokens dentro do corpo do componente, então os valores sempre refletem o tema em tempo real. O mesmo mapa de tokens também é exportado como a constante `themeCssVariables`, mas prefira `useTheme()` em componentes de front-end — uma constante em nível de módulo que desreferencia `themeCssVariables` pode ser indefinida enquanto o manifesto do app é extraído.
Para diferenciar explicitamente com base no esquema ativo, leia-o com `useColorScheme()` de `twenty-sdk/front-component`, que retorna 'light' ou 'dark'.
## Limitações atuais
Componentes frontais estão em desenvolvimento ativo. Renderização, estilização, tratamento de eventos, medição de elementos e armazenamento no navegador funcionam bem. Qualquer coisa que vá além disso (chamar um método do DOM em uma ref, observar redimensionamentos de elementos, criar um portal para fora da sua árvore) está ausente ou incompleta hoje, e a maioria falha silenciosamente: sem exceção nem erro de TypeScript, já que o arcabouço é tipado com base no DOM completo do navegador.
Se um desses blocos, [abra um issue](https://github.com/twentyhq/twenty/issues/new/choose) para que seja priorizado.
### Layout e medição
Os elementos podem se medir: o host espelha a geometria no sandbox, então as leituras são respondidas localmente, mas podem estar defasadas em até um quadro, e a primeira leitura de um elemento nunca medido retorna zeros. Depois de escrever, releia em um callback de `requestAnimationFrame` ou em um efeito.
| API | O que acontece |
| ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `getBoundingClientRect()`, `offsetWidth`, `clientWidth`, `scrollWidth`, `offsetTop`, ... | Funciona, a partir do espelho; atribuir `scrollTop` / `scrollLeft` não faz nada (no-op) |
| `getClientRects()`, `window.matchMedia()` | Lançamento |
| `window.innerWidth`, `innerHeight`, `devicePixelRatio`, `scrollX`, `scrollY` | Funciona, relatando o **viewport do navegador**; o tamanho próprio do seu widget é `document.body.clientWidth` / `clientHeight` |
| `window.getComputedStyle()` | Retorna apenas os estilos inline do elemento, nunca a cascata calculada pelo host |
| `ResizeObserver`, `IntersectionObserver` | `ReferenceError` (`typeof` guardas fazem trabalho) |
| `MutationObserver` | Funciona, incluindo `subtree`, `attributeFilter`, valores antigos e `takeRecords()` |
O posicionamento a partir de `getBoundingClientRect` agora funciona, mas qualquer coisa que observe mudanças de tamanho por meio de `ResizeObserver` (o `ResponsiveContainer` do recharts, o `autoUpdate` do Floating UI) ainda não funciona. Ainda assim, prefira CSS para layout: sua folha de estilos alcança a página real, então flexbox, grid, `aspect-ratio`, `clamp()` e `@container` se comportam normalmente, sem atraso de frame.
<Note>
`requestAnimationFrame`, `fetch`, `setTimeout` and `queueMicrotask` trabalham sem o prefixo `window.`. Apenas `window.requestAnimationFrame(...)` e amigos lançam.
</Note>
### DOM access
Um `ref` fornece um elemento da sandbox, não um `HTMLElement`.
| O que você escreve | O que acontece | Usar em vez disso |
| ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ref.current.focus()`, `.click()`, `.select()`, `.setSelectionRange()`, `.scrollIntoView()`, `video.play()` | Lançamento | Componentes controlados; leia valores de `event.target` |
| `element.classList.add(...)` | Lança (`classList` é `undefined`) | Construa a string `className` |
| `document.createTreeWalker()` | Lançamento | `querySelector()` / `querySelectorAll()` e `getElementById()` funcionam; `getElementsByClassName()` também funciona, mas retorna uma coleção estática, não uma `HTMLCollection` dinâmica |
| `document.activeElement` | Sempre `indefinido` | Rastreie o foco com `onFocus` / `onBlur` |
| `canvas` | Não renderiza nada, sem erro | SVG ou desenhar fora da tela e mostrar o resultado em um elemento `img` |
| `createPortal(node, document.body)` | Não renderiza nada, enquanto `isConnected` reporta sucesso | Sobrepõe em linha com `posição: absoluto`, ou passa a biblioteca seu próprio elemento de contêiner |
A lacuna do portal é o motivo da Radix, da interface do usuário, MUI e da interface de reacção, que não tornam nada por padrão. A maioria aceita uma propriedade de container; aponta-a para um elemento que você renderizou.
### Eventos
Mouse, pointer, touch, drag, teclado, foco, `input`/`change`/`submit`, `scroll`/`wheel`/`contextmenu` e `animationend`/`transitionend` são encaminhados ao host, além de alguns específicos por elemento: `load`/`error` em `img`, área de transferência e composição em `input`/`textarea`, mídia em `video`/`audio`, `toggle` em `details`/`dialog`. Qualquer outra coisa (`onAuxClick`, `onSelect`, `onInvalid`, `onReset`, `onAnimationStart`, captura de ponteiro, `onLoad` fora de `img`) é descartada sem aviso.
`document.addEventListener()` e `janela. ddEventListener()` registra-se sem erro e nunca dispara, é por isso que um arrastar para o mesmo assim que o ponteiro deixa o elemento no qual ele iniciou. `event.preventDefault()` também não atravessa; formulário de envio, `dragover`/`drop` e cliques de links já estão guardados para você.
### Atributos e estilos
Cada elemento encaminha suas próprias propriedades para o DOM host (`href` em `a`, `src`/`alt` em `img`, `value`/`placeholder`/`disabled` em `input` e assim por diante), além de um conjunto comum em todos os elementos: `id`, `className`, `style`, `title`, `tabIndex`, `role`, `draggable` e qualquer atributo `aria-*` / `data-*` (com hífen, portanto `ariaLabel` é descartado). Qualquer coisa fora que seja silenciosamente descartada, então expressa estado personalizado como `data-*`.
O CSS do componente, seja de `import './styles.css'`, CSS-in-JS ou um elemento `style`, é injetado no `head` da página host **sem escopo**. Então os nomes das classes colidem com os próprios do Twenty (prefixe-os, e nunca escrevem bare `div { ... }` seletores) e `@media` coincidem com a janela do navegador ao invés do seu widget (use `@container` com seu próprio `container-type`). Propriedades `style` embutidas não são afetadas.
### Armazenamento e rede
`localStorage` e `sessionStorage` são fornecidos pela Twenty em vez do navegador: o componente é executado em um worker em uma origem opaca, portanto o host armazena os valores em nome do seu app. Veja [armazenamento](#storage) para saber mais sobre seu escopo e limites. IndexedDB, cookies, a Cache API e `BroadcastChannel` continuam indisponíveis. Para manter o estado entre dispositivos, chame uma [função de lógica](/l/pt/developers/extend/apps/logic/logic-functions) e use seu [armazenamento de chave-valor](/l/pt/developers/extend/apps/logic/key-value-store).
`fetch` funciona, com advertências:
* Chamadas para a API de Vinte e as rotas do seu aplicativo são procuradas pelo host, então prefira [`RestApiClient`](#calling-the-twenty-rest-api). Em chamadas de proxied, `AbortSignal` e as outras opções `RequestInit` são descartadas, e apenas os corpos `string` e `URLSearchParams` são suportados.
* Outras origens deixam o sandbox com `Origin: null`, então uma API de terceiros responde apenas se envia `Access-Control-Allow-Origin: *`. Chame-a de uma função lógica.
* `fetch('/rest/people')` nunca corresponde a 20 API, porque o sandbox não tem URL de página para resolver um caminho relativo contra.
### Captura de mídia
`navigator.mediaDevices.getUserMedia()` e `MediaRecorder` funcionam dentro de front components por meio de sandbox polyfills, portanto o código padrão de gravação é executado sem alterações e `MediaRecorder.isTypeSupported` responde para combinações comuns de contêiner/codec. Objetos detalhados de restrição de `getUserMedia` são aceitos, mas não encaminhados — o host captura com seus padrões para os tipos solicitados — e apenas uma captura pode estar ativa por vez entre aplicativos. Armazene um `Blob` gravado com a função host `uploadFile`.
### Outras lacunas
* **Conteúdo de arquivo.** Um `input` do tipo `file` fornece ao seu manipulador apenas os metadados do arquivo, não os bytes, portanto `FileReader` não está disponível. Para fazer upload de um `Blob` que seu código já possui — por exemplo, um produzido por `MediaRecorder` — use a função host `uploadFile`.
* **Arrastar e soltar payloads.** Arraste eventos disparados, mas `event.dataTransfer` é `undefined`.
* **Node embutidos.** `fs`, `path` and `node:crypto` falham na compilação, então mova o nó para uma [função lógica](/l/pt/developers/extend/apps/logic/logic-functions). Criptografia Web, `fetch`, `TextEncoder` e `URL` estão disponíveis.
* **`iframe`** é sempre colocado novamente em sandbox sem `allow-same-origin`, portanto uma incorporação que dependa da própria sessão será renderizada como desconectada. Isso também não tem `onLoad`.

View File

@@ -0,0 +1,46 @@
---
title: Itens do menu de navegação
description: Adicione entradas personalizadas à barra lateral do espaço de trabalho — links para visualizações salvas ou URLs externas.
icon: bars
---
Um **item do menu de navegação** é uma entrada na barra lateral esquerda. Use `defineNavigationMenuItem()` para disponibilizar links personalizados na barra lateral — normalmente um por [visualização](/l/pt/developers/extend/apps/layout/views) que você disponibiliza — ou para apontar para URLs externas.
```ts src/navigation-menu-items/example-navigation-menu-item.ts
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define';
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 onde o item de menu aponta. Cada tipo é associado a um campo identificador específico:
| Tipo | O que faz | Campo obrigatório |
| ------------------------------------ | ------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `NavigationMenuItemType.VIEW` | Abre uma visualização salva | `viewUniversalIdentifier` |
| `NavigationMenuItemType.LINK` | Abre uma URL externa | `link` |
| `NavigationMenuItemType.FOLDER` | Agrupa itens aninhados sob um rótulo | `name` (e itens filhos fazem referência à pasta por meio de `folderUniversalIdentifier`) |
| `NavigationMenuItemType.OBJECT` | Abre a página de índice padrão de um objeto | `targetObjectUniversalIdentifier` |
| `NavigationMenuItemType.PAGE_LAYOUT` | Abre um layout de página independente | `pageLayoutUniversalIdentifier` |
* `position` controla a ordenação na barra lateral.
* O enum também contém `NavigationMenuItemType.RECORD`, usado internamente para favoritos de registros criados pelo usuário — não pode ser usado a partir de um manifesto de app (não há nenhum campo para fazer referência a um registro).
* `icon` e `color` são opcionais e personalizam a aparência da entrada.
* `folderUniversalIdentifier` também está disponível em qualquer item para aninhá-lo dentro de um pai do tipo `FOLDER`.
<Note>
**Armadilha comum:** criar um objeto sem um item de menu de navegação associado torna esse objeto inacessível a partir da barra lateral. A menos que seja um objeto técnico/interno, todo objeto personalizado deve ter uma entrada na barra lateral apontando para ele.
</Note>

View File

@@ -0,0 +1,59 @@
---
title: Visão Geral
description: Coloque seu app dentro da interface do Twenty — entradas na barra lateral, visualizações salvas, abas na página de registro e componentes React em sandbox.
icon: table-columns
---
A **camada de layout** de um app do Twenty é tudo o que o usuário vê: onde o app aparece na barra lateral, quais visualizações de lista ele fornece, como suas páginas de detalhes de registro são organizadas e quais componentes React personalizados são renderizados dentro dessas páginas.
```text
Sidebar Record list Record detail page
─────── ─────────── ──────────────────
[📋 My View] ────▶ ┌──────────┐ ┌─────────────────────┐
[📋 Drafts ] │ Companies│ │ Tabs: [Overview ] │
[📋 Inbox ] │ ──────── │ │ [Notes ] │
▲ │ Apple │ │ [Hello ]◀──── definePageLayoutTab
│ │ Acme │ │ │ adds a tab...
└ defineNavi- │ … │ │ ┌────────────────┐ │
gationMenu- └────▲─────┘ │ │ │ │
Item points │ │ │ React UI │◀── …with a
to a defineView │ │ │ (sandboxed in │ │ defineFrontComponent
└ defineView │ │ a Worker) │ │ widget inside
picks columns │ └────────────────┘ │
and filters └─────────────────────┘
```
## Nesta seção
<CardGroup cols={2}>
<Card title="Visualizações" icon="list" href="/l/pt/developers/extend/apps/layout/views">
`defineView` — configurações salvas de lista: colunas visíveis, filtros, grupos.
</Card>
<Card title="Itens do menu de navegação" icon="bars" href="/l/pt/developers/extend/apps/layout/navigation-menu-items">
`defineNavigationMenuItem` — entradas da barra lateral apontando para visualizações ou URLs externas.
</Card>
<Card title="Layouts de Página" icon="table-columns" href="/l/pt/developers/extend/apps/layout/page-layouts">
`definePageLayout` e `definePageLayoutTab` — abas e widgets na página de detalhes de um registro.
</Card>
<Card title="Componentes de front-end" icon="window-maximize" href="/l/pt/developers/extend/apps/layout/front-components">
`defineFrontComponent` — componentes React em sandbox que são renderizados dentro do Twenty.
</Card>
<Card title="Itens do menu de comandos" icon="terminal" href="/l/pt/developers/extend/apps/layout/command-menu-items">
`defineCommandMenuItem` — registra componentes de front-end como entradas no Cmd+K e ações rápidas.
</Card>
</CardGroup>
## Onde o app aparece
| Superfície | O que controla | Entidade |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
| **Barra lateral** | Uma entrada personalizada que aponta para uma visualização salva ou URL externa | `defineNavigationMenuItem` |
| **Lista de registros** | Uma configuração salva para um objeto — colunas visíveis, ordem, filtros, grupos | `defineView` |
| **Página de detalhes do registro** | As abas e widgets em uma página de registro (do seu próprio objeto ou de um objeto padrão) | `definePageLayout`, `definePageLayoutTab` |
| **Dentro de qualquer uma das opções acima** | Um widget React personalizado — botões, formulários, dashboards, integrações | `defineFrontComponent` |
| **Configurações do app** | Uma seção de configuração personalizada dentro da aba Configurações do app, em vez da interface padrão de variáveis | `defineSettingsFrontComponent` |
| **Menu de comandos (Cmd+K)** | Uma ação rápida fixada ou comando oculto | `defineCommandMenuItem` |
Os componentes de front-end são executados dentro de um Web Worker isolado usando Remote DOM — eles são renderizados *nativamente* na página (não dentro de um iframe), mas não podem acessar diretamente a página host ou o DOM. A comunicação com o Twenty acontece por meio de uma API de host com passagem de mensagens.
Eles ainda estão em desenvolvimento ativo: o sandbox implementa um DOM parcial, portanto usos avançados podem falhar. Veja as [limitações atuais](/l/pt/developers/extend/apps/layout/front-components#current-limitations).

View File

@@ -0,0 +1,158 @@
---
title: Layouts de Página
description: Personalize páginas de detalhes de registros — abas, widgets e onde os front components são renderizados — usando `definePageLayout` e `definePageLayoutTab`.
icon: table-columns
---
Um **layout de página** controla como a página de detalhes de um registro é organizada: quais abas aparecem e quais widgets elas contêm. Use `definePageLayout()` para declarar um layout para um objeto que você possui ou `definePageLayoutTab()` para adicionar uma única aba a um layout que já existe (seu ou um padrão da Twenty).
| Caso de uso | Entidade |
| ------------------------------------------------------------------------------ | --------------------- |
| Definir todo o layout para uma página de registro em um objeto que você possui | `definePageLayout` |
| Adicionar uma aba a um layout existente (seu próprio objeto ou um padrão) | `definePageLayoutTab` |
## definePageLayout
Use isto quando você possuir toda a página de detalhes — normalmente para um objeto personalizado que você próprio definiu.
```ts src/page-layouts/example-record-page-layout.ts
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
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.VERTICAL_LIST,
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` é um dos seguintes: `'RECORD_INDEX'`, `'RECORD_PAGE'`, `'DASHBOARD'` ou `'STANDALONE_PAGE'`. Use `'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`: `VERTICAL_LIST` para páginas de registro e páginas independentes, `GRID` para dashboards e `CANVAS` para um único widget que deve preencher a janela de visualização da aba. Uma aba `VERTICAL_LIST` empilha widgets verticalmente. Widgets integrados que gerenciam sua própria rolagem, como linhas do tempo, arquivos, notas, tarefas e fluxos de trabalho, preenchem uma janela de visualização; campos, componentes de front, gráficos e outros widgets fit-content são renderizados na altura de seu conteúdo ou na altura configurada. Uma aba `GRID` sempre organiza seus widgets como cartões em uma grade de 12 colunas. Um widget `CANVAS` não tem posição explícita; se uma aba de canvas contiver vários widgets, eles serão renderizados na altura do conteúdo, em vez de preencherem a viewport.
* Defina `layoutMode` explicitamente. Omití-lo faz com que você obtenha `VERTICAL_LIST` em uma `STANDALONE_PAGE` e `GRID` em todos os outros casos, o que raramente é o que você deseja em uma página de registro.
* Cada `widget` dentro de uma aba pode renderizar um [front component](/l/pt/developers/extend/apps/layout/front-components), uma lista de relações ou outros tipos de widget nativos.
* Um widget `FRONT_COMPONENT` pode definir `headerCommandMenuItemUniversalIdentifiers` como uma matriz ordenada de identificadores universais de itens de menu de comandos do mesmo app. Essas ações aparecem como botões de ícone no cabeçalho do cartão do widget e mantêm suas verificações de disponibilidade e permissões no nível do comando. Os identificadores devem ser exclusivos e devem ser resolvidos quando o app for instalado.
* `position` nas abas controla sua ordem. Use valores mais altos (por exemplo, 50) para colocar abas personalizadas após as nativas.
### Widgets de campos
Um widget `FIELD` renderiza um campo do registro. Para campos de relação, ele também pode incorporar uma lista de registros relacionados:
```ts
{
universalIdentifier: 'c1c2c3c4-c5c6-4000-8000-000000000003',
title: 'People → Opportunities',
type: 'FIELD',
configuration: {
configurationType: 'FIELD',
fieldMetadataId: PEOPLE_FIELD_UNIVERSAL_IDENTIFIER,
fieldDisplayMode: 'TABLE',
nestedRelationFieldMetadataId: OPPORTUNITIES_FIELD_UNIVERSAL_IDENTIFIER,
},
}
```
* `fieldMetadataId` recebe o identificador universal de um campo no objeto do layout.
* `fieldDisplayMode` é um dentre `'FIELD'`, `'CARD'`, `'EDITOR'`, `'VIEW'` ou `'TABLE'`. `TABLE` incorpora uma visualização que lista os registros de um campo de relação um-para-muitos.
* `nestedRelationFieldMetadataId` é opcional e recebe o identificador universal de um campo de relação um-para-muitos no objeto de destino da relação, para listar registros a dois saltos de relação (por exemplo, uma página de Empresa listando as oportunidades das pessoas da empresa, ou uma página de Pessoa listando as oportunidades da empresa da pessoa). O primeiro salto pode ser um campo de relação um-para-muitos ou muitos-para-um, o segundo deve ser um-para-muitos (relações de junção não são compatíveis) e isso requer `fieldDisplayMode: 'TABLE'` — combiná-lo com qualquer outro modo de exibição é um erro de validação, já que um widget aninhado sempre é renderizado como uma visualização incorporada.
## definePageLayoutTab
Use isto quando você quiser apenas **adicionar** uma aba a um layout existente — por exemplo, uma aba de analytics na página padrão de Company ou uma aba de resumo de IA anexada ao layout do seu próprio objeto.
```ts src/page-layouts/example-extra-tab.ts
import {
definePageLayoutTab,
PageLayoutTabLayoutMode,
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
export default definePageLayoutTab({
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001',
pageLayoutUniversalIdentifier:
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage
.universalIdentifier,
title: 'Hello World',
position: 1000,
icon: 'IconWorld',
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
widgets: [
{
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000002',
title: 'Hello World',
type: 'FRONT_COMPONENT',
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier:
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
},
],
});
```
### Pontos-chave
* `pageLayoutUniversalIdentifier` é **obrigatório** e deve apontar para um layout de página que já exista no momento da instalação — seja um layout padrão da Twenty ou um definido pelo seu próprio aplicativo. Referências entre aplicativos para layouts pertencentes a outro aplicativo instalado não são compatíveis atualmente. Quando o layout pai estiver ausente, a instalação falha com um erro de validação claro.
* Para layouts padrão do Twenty, importe identificadores de `twenty-sdk/define`:
```ts
import { STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.personRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.opportunityRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.noteRecordPage.universalIdentifier
// …
```
Cada entrada de layout também expõe suas `tabs` e seus `widgets`, para que você possa fazer referência a qualquer nível:
```ts
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home
.universalIdentifier;
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.widgets
.fields.universalIdentifier;
```
Um alias abreviado `STANDARD_PAGE_LAYOUT` também está disponível:
```ts
import { STANDARD_PAGE_LAYOUT } from 'twenty-sdk/define';
STANDARD_PAGE_LAYOUT.companyRecordPage.universalIdentifier;
```
* `widgets` têm escopo apenas para esta aba — eles referenciam [front components](/l/pt/developers/extend/apps/layout/front-components), visualizações etc., exatamente como widgets definidos inline em `definePageLayout`.
* `position` controla a ordenação em relação às abas existentes no layout de destino. Escolha um valor que posicione sua aba onde você deseja em relação às abas nativas.
* Use isto em vez de `definePageLayout` quando você quiser apenas adicionar a um layout existente. Use `definePageLayout` quando você possuir todo o layout.

View File

@@ -0,0 +1,113 @@
---
title: Visualizações
description: Distribua visualizações salvas e pré-configuradas — ordem das colunas, filtros, grupos — para objetos no seu app.
icon: list
---
Uma **visualização** é uma configuração salva de como os registros de um objeto são exibidos: quais campos aparecem, sua ordem, se estão visíveis e quaisquer filtros ou grupos aplicados. Use `defineView()` para enviar visualizações pré-configuradas com seu app.
Cada objeto recebe automaticamente sua principal visualização de lista (a tabela "All ..." que um item de navegação `OBJECT` abre), provisionada pelo servidor — incluindo os objetos que seu app cria, com uma coluna por campo que seu app declara. As visualizações que você define com `defineView()` são sempre visualizações **adicionais** em cima daquela padrão.
```ts src/views/example-view.ts
import { defineView } from 'twenty-sdk/define';
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',
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. Pode ser um objeto personalizado que você definiu ou um objeto padrão do Twenty.
* A visualização principal de lista do objeto é de propriedade do servidor: `key` está obsoleto e é ignorado, portanto uma visualização definida no manifesto nunca poderá reivindicá-la. Envie um item de navegação `VIEW` se quiser que sua visualização fique a um clique de distância na barra lateral.
* `fields` controla quais colunas aparecem e em que ordem. Cada campo referencia um `fieldMetadataUniversalIdentifier`. Para fazer referência a um campo de sistema criado automaticamente, como `createdAt`, consulte [Direcionamento de metadados do sistema](/l/pt/developers/extend/apps/data/system-fields).
* Você também pode declarar `filters`, `filterGroups`, `sorts`, `groups` e `fieldGroups` para configurações avançadas.
* `position` controla a ordenação quando existem várias visualizações para o mesmo objeto.
## Propriedades opcionais
| Propriedade | Valores | Descrição |
| ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | `ViewType.TABLE` (padrão), `ViewType.LIST`, `ViewType.KANBAN`, `ViewType.CALENDAR` | Como os registros são dispostos. (`FIELDS_WIDGET`, `TABLE_WIDGET`, `KANBAN_WIDGET`, `LIST_WIDGET` e `CALENDAR_WIDGET` também existem, mas são usados internamente por widgets de layout de página.) |
| `visibility` | `ViewVisibility.WORKSPACE` (padrão), `ViewVisibility.UNLISTED` | Se a visualização é listada em todo o espaço de trabalho ou oculta nos seletores. |
| `openRecordIn` | obsoleto | Não é mais lido: o local onde os registros são abertos agora é uma propriedade do [objeto](/l/pt/developers/extend/apps/data/objects) (`openRecordIn` em `defineObject()`), recorrendo à preferência de cada membro. |
| `ordenações` | `{ fieldMetadataUniversalIdentifier, direction: ViewSortDirection.ASC \| DESC }[]` | Ordem de classificação padrão. |
| `isCompact` | `boolean` | Exibição compacta de linhas. |
| `mainGroupByFieldMetadataUniversalIdentifier` + `shouldHideEmptyGroups` | — | Agrupar registros (por exemplo, colunas kanban) por um campo. |
| `kanbanAggregateOperation`, `kanbanAggregateOperationFieldMetadataUniversalIdentifier`, `kanbanColumnWidth` | `AggregateOperations.*` | Agregações e dimensionamento de colunas kanban. |
| `calendarLayout`, `calendarFieldMetadataUniversalIdentifier`, `calendarEndFieldMetadataUniversalIdentifier` | `ViewCalendarLayout.DAY` / `WEEK` / `MONTH` | Visualizações de calendário: layout, o campo de data que posiciona os registros e um campo de data final opcional. |
Todos os enums acima são exportados de `twenty-sdk/define`.
## Filtros
Uma visualização pode vir com filtros pré-aplicados. Cada filtro tem três coordenadas: o **campo** a ser filtrado, o **operador** (como comparar) e o **valor** (com o que comparar). As três precisam estar alinhadas — usar um operador que não se aplica a um tipo de campo será rejeitado no momento da sincronização.
```ts
import { ViewFilterOperand } from 'twenty-sdk/define';
filters: [
{
universalIdentifier: '...',
fieldMetadataUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
operand: ViewFilterOperand.IS,
value: ['ACTIVE'],
},
],
```
### Operadores compatíveis por tipo de campo
| Tipo de campo | Operandos suportados |
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `TEXT`, `EMAILS`, `FULL_NAME`, `ADDRESS`, `LINKS`, `PHONES`, `RAW_JSON`, `FILES`, `ACTOR`, `ARRAY` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `ACTOR.source`, `ACTOR.workspaceMemberId` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `SELECT` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `MULTI_SELECT` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RELATION` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `NUMBER` | `IS`, `IS_NOT`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RATING` | `IS`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY`, `CURRENCY.amountMicros` | `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `BOOLEAN` | `IS` |
| `UUID` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `TS_VECTOR` | `VECTOR_SEARCH` |
> Tipos de campos com nomes semelhantes podem usar operandos completamente diferentes — `SELECT` e `MULTI_SELECT` sendo um caso comum.
### Formato do valor por operando
O campo `value` é sempre um valor serializável em JSON, mas o seu formato esperado depende do operando:
| Família do operando | Formato do valor | Exemplo |
| ----------------------------------------------------- | ----------------------------------- | ------------------------ |
| `IS`, `IS_NOT` em `SELECT` | array de chaves de opções (strings) | `['ACTIVE', 'PENDING']` |
| `CONTAINS`, `DOES_NOT_CONTAIN` em `MULTI_SELECT` | array de chaves de opções (strings) | `['TAG_A']` |
| `IS`, `IS_NOT` em `RELATION` | array de IDs de registros (uuids) | `['c5a1...']` |
| `CONTAINS`, `DOES_NOT_CONTAIN` em campos de texto | string | `'acme'` |
| `IS`, `IS_NOT` em `NUMBER` | string (o valor) | `'5'` |
| `IS` em `RATING` / `UUID` | string (o valor) | `'5'` |
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | string (o limite) | `'10'` |
| `IS`, `IS_BEFORE`, `IS_AFTER` em `DATE` / `DATE_TIME` | string ISO 8601 | `'2025-01-01T00:00:00Z'` |
| `IS_EMPTY`, `IS_NOT_EMPTY` | string vazia | `''` |
| `IS` em `BOOLEAN` | `'true'` ou `'false'` | `'true'` |
## Como as visualizações aparecem na UI
Uma visualização por si só não é acessível a partir da barra lateral. Para fazê-la aparecer lá, associe-a a um [item de menu de navegação](/l/pt/developers/extend/apps/layout/navigation-menu-items) do tipo `VIEW` que aponte para o `universalIdentifier` da visualização. Esse é o padrão canônico: todo objeto personalizado recebe uma exibição de índice padrão gerada automaticamente no momento da criação, então você só precisa incluir o item da barra lateral que a abre.

View File

@@ -0,0 +1,197 @@
---
title: Tarefas em segundo plano
description: Entregue trabalhos longos ou com limitação de taxa aos workers do Twenty colocando na fila outra execução de função de lógica, em vez de fazer tudo inline.
icon: layer-group
---
Uma execução de função de lógica é limitada pelo seu `timeoutSeconds` (máximo de 900 segundos). Qualquer coisa que não consiga terminar nesse intervalo — uma re-sincronização completa, uma distribuição por registro, uma API de terceiros que impõe limitações de taxa — precisa ser dividida em execuções menores.
`enqueueJobs` faz exatamente isso: solicita aos workers do Twenty que executem mais tarde uma das funções de lógica do seu aplicativo, uma vez por payload, cada execução em seu próprio processo com seu próprio orçamento de tempo limite. O chamador retorna imediatamente.
```text
┌─────────────────┐ enqueueJobs(...) ┌──────────────┐ ┌────────────────────┐
│ Logic function │ ─────────────────▶ │ Job queue │──▶│ Logic function │
│ (returns now) │ │ (workers) │ │ (fresh run/timeout)│
└─────────────────┘ └──────────────┘ └────────────────────┘
```
## Colocar execuções na fila
Importe `enqueueJobs` de `twenty-sdk/logic-function`, aponte-o para o `universalIdentifier` da função de lógica a ser executada e passe um payload por execução para colocar na fila.
```ts src/logic-functions/sync-all-contacts.ts
import { enqueueJobs } from 'twenty-sdk/logic-function';
await enqueueJobs({
logicFunctionUniversalIdentifier: '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33',
payloads: [{ page: 1 }],
});
```
Cada execução recebe seu payload como argumento do handler, exatamente como qualquer outro gatilho. O destino deve pertencer à **mesma aplicação** que quem o chama — colocar na fila a função de outra aplicação é rejeitado com `Logic function not found` e nada é colocado na fila. Uma única chamada aceita até `200` payloads.
<Note>
`enqueueJobs` retorna assim que os jobs são aceitos, não quando são executados. Ele não retorna os resultados dos destinos — faça cada destino gravar o que produzir no [repositório de chave-valor](/l/pt/developers/extend/apps/logic/key-value-store) ou em um registro do workspace se você precisar ler isso de volta.
</Note>
<Note>
O auxiliar `enqueueJob` mais antigo, que coloca um único job na fila por chamada, está obsoleto. Use `enqueueJobs` com uma lista `payloads` de um elemento em vez disso.
</Note>
## Opções do job
As opções se aplicam a todas as execuções no lote.
| Opção | Padrão | Intervalo | O que faz |
| ------------ | ------ | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `retryLimit` | `0` | `0`–`10` | Total de tentativas adicionais na fila. As novas tentativas solicitadas pelo aplicativo são limitadas a `3`. Só aumente isso para handlers que sejam seguros para serem executados duas vezes. |
| `delayMs` | `0` | `0`–`604800000` (7 dias) | Espere esse tempo antes de as execuções se tornarem elegíveis. |
```ts
await enqueueJobs({
logicFunctionUniversalIdentifier: '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33',
payloads: [{ page: 1 }],
retryLimit: 3,
delayMs: 60_000,
});
```
<Note>
**A prioridade ainda não é configurável.** Jobs em fila sempre são executados na prioridade mais baixa, então o trabalho da plataforma nunca é atrasado por jobs da aplicação. O controle sobre prioridade estará disponível em breve.
</Note>
A execução em fila herda o usuário ativo da função que a colocou na fila, portanto age com as mesmas permissões.
## Tentar novamente uma falha transitória
O Twenty não tenta novamente todas as exceções do código da aplicação. Um erro comum lançado é tratado como uma falha permanente. Para uma falha transitória, uma função lógica enfileirada pode solicitar até três novas tentativas lançando `RetryableLogicFunctionError`.
```ts
import {
type LogicFunctionExecutionContext,
RetryableLogicFunctionError,
} from 'twenty-sdk/logic-function';
export const handler = async (
_payload: unknown,
{ retryCount, maxRetries }: LogicFunctionExecutionContext,
) => {
const response = await fetch('https://api.example.com/contacts');
if (response.status === 429 || response.status >= 500) {
throw new RetryableLogicFunctionError(
`The contacts API is temporarily unavailable (${response.status}); retry ${retryCount} of ${maxRetries}`,
);
}
};
```
Lance `RetryableLogicFunctionError` diretamente quando possível. Se você estendê-lo, não substitua seu `name`: o Twenty reconhece o nome serializado `RetryableLogicFunctionError` em todos os ambientes de execução.
`retryCount` é `0` para a execução inicial e aumenta apenas quando o código da aplicação solicita uma nova tentativa. `maxRetries` é no máximo `3` e pode ser menor quando o trabalho enfileirado tem um limite geral de novas tentativas menor. As falhas da plataforma não aumentam `retryCount`, embora ainda consumam o orçamento geral de segurança da fila.
A fila atrasa as tentativas de repetição com recuo exponencial e jitter. O atraso exato não é garantido intencionalmente, portanto o código da aplicação não deve depender de uma nova tentativa ocorrer em um momento preciso. Quando `maxRetries` é atingido, outro `RetryableLogicFunctionError` é registrado como a falha final da aplicação sem outra execução.
<Warning>
As novas tentativas executam novamente todo o manipulador e podem ocorrer depois que alguns efeitos colaterais foram bem-sucedidos. Torne o manipulador idempotente antes de solicitar novas tentativas.
</Warning>
## Use assim: pagine uma sincronização longa
O formato clássico é uma função que coloca *a si mesma* na fila com o próximo cursor. Cada execução faz uma página de trabalho bem dentro do seu próprio tempo limite, e a cadeia para quando não resta nada.
```ts src/logic-functions/sync-contacts-page.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { enqueueJobs } from 'twenty-sdk/logic-function';
const SYNC_CONTACTS_PAGE = '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33';
const handler = async (params: { cursor?: string }) => {
const { contacts, nextCursor } = await fetchContactsPage(params.cursor);
await importContacts(contacts);
if (nextCursor) {
await enqueueJobs({
logicFunctionUniversalIdentifier: SYNC_CONTACTS_PAGE,
payloads: [{ cursor: nextCursor }],
delayMs: 2_000,
});
}
return { imported: contacts.length, done: !nextCursor };
};
export default defineLogicFunction({
universalIdentifier: SYNC_CONTACTS_PAGE,
name: 'sync-contacts-page',
timeoutSeconds: 120,
handler,
});
```
## Ramificar por registro
Quando o trabalho é naturalmente por item, coloque um job por item na fila em uma única chamada e deixe os workers processá-los em paralelo em vez de fazer o loop inline.
```ts
const companies = await listCompaniesToEnrich();
await enqueueJobs({
logicFunctionUniversalIdentifier: ENRICH_COMPANY,
payloads: companies.map((company) => ({ companyId: company.id })),
retryLimit: 2,
});
```
## Boas práticas para trabalho de longa duração
Duas regras cobrem quase todo job longo: **faça recursão em vez de loop** e **processe um fragmento limitado por execução**.
Uma execução que tenta fazer tudo é o modo de falha — ela atinge o tempo limite e, com uma nova tentativa, começa tudo de novo do zero. Em vez disso, defina o tamanho de um fragmento para que ele termine confortavelmente dentro de `timeoutSeconds`, persista sua posição e coloque a próxima execução na fila.
```ts src/logic-functions/enrich-companies-batch.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { enqueueJobs, kv } from 'twenty-sdk/logic-function';
const ENRICH_COMPANIES_BATCH = '3f9d1c02-8a44-4f0e-b1d7-9c2e5a7b4f10';
const CHUNK_SIZE = 50;
const handler = async (params: { offset?: number }) => {
const offset = params.offset ?? 0;
const companies = await listCompaniesToEnrich({
offset,
limit: CHUNK_SIZE,
});
for (const company of companies) {
await enrichCompany(company);
}
await kv.set('enrich:progress', { offset: offset + companies.length });
if (companies.length === CHUNK_SIZE) {
await enqueueJobs({
logicFunctionUniversalIdentifier: ENRICH_COMPANIES_BATCH,
payloads: [{ offset: offset + CHUNK_SIZE }],
});
}
return { processed: companies.length, done: companies.length < CHUNK_SIZE };
};
export default defineLogicFunction({
universalIdentifier: ENRICH_COMPANIES_BATCH,
name: 'enrich-companies-batch',
timeoutSeconds: 300,
handler,
});
```
O que torna isso robusto:
* **Defina o tamanho do fragmento a partir do item mais lento, não da média.** `CHUNK_SIZE × tempo do item em pior caso` precisa caber em `timeoutSeconds` com alguma folga, ou o final de um fragmento é perdido quando a execução é interrompida.
* **Deixe a condição de parada explícita.** Faça recursão apenas enquanto um fragmento completo for retornado. Uma cadeia que para apenas em "sem resultados" continuará para sempre se a origem algum dia retornar uma página curta no meio do caminho.
* **Persista o progresso antes de colocar a próxima execução na fila,** assim um elo com falha reinicia a partir do último fragmento concluído em vez do começo.
* **Mantenha cada fragmento idempotente.** Reprocessar um fragmento após uma nova tentativa não deve gerar escrita em dobro — faça as gravações com base no registro ou id externo que você está processando.
* **Prefira uma cadeia fragmentada em vez de uma grande ramificação** quando o trabalho aciona um serviço de terceiros com limitação de taxa: uma cadeia com `delayMs` se regula sozinha, enquanto milhares de jobs colocados na fila de uma vez se tornam elegíveis imediatamente.

View File

@@ -0,0 +1,269 @@
---
title: Conexões
description: Permita que seu aplicativo aja em nome de um usuário em serviços de terceiros via OAuth.
icon: plug
---
Conexões são credenciais que um usuário mantém para um serviço externo (Linear, GitHub, Slack, ...). Seu app declara **como** essas credenciais são obtidas — um **provedor de conexão** — e as consome em tempo de execução para fazer chamadas autenticadas à API de terceiros.
Atualmente, apenas o OAuth 2.0 tem suporte. Tipos de credenciais futuros (tokens de acesso pessoal, chaves de API, autenticação básica) serão conectados à mesma interface — apps que já usam `defineConnectionProvider({ type: 'oauth', ... })` não precisarão migrar.
<AccordionGroup>
<Accordion title="defineConnectionProvider" description="Declare como as conexões do seu app são obtidas">
Um provedor de conexão descreve o handshake OAuth de que seu app precisa. O usuário clica em "Adicionar conexão" nas configurações do seu app, conclui a tela de consentimento do provedor e uma linha `ConnectedAccount` é criada no seu workspace.
Uma configuração funcional precisa de **dois arquivos** — o provedor de conexão e uma declaração correspondente de `serverVariables` em `defineApplication` que contém as credenciais do cliente OAuth.
```ts src/connection-providers/linear-connection.ts
import { defineConnectionProvider } from 'twenty-sdk/define';
export default defineConnectionProvider({
universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f',
name: 'linear',
displayName: 'Linear',
icon: 'IconBrandLinear',
type: 'oauth',
oauth: {
authorizationEndpoint: 'https://linear.app/oauth/authorize',
tokenEndpoint: 'https://api.linear.app/oauth/token',
scopes: ['read', 'write'],
// These must match keys in `defineApplication.serverVariables` below.
clientIdVariable: 'LINEAR_CLIENT_ID',
clientSecretVariable: 'LINEAR_CLIENT_SECRET',
// Optional: defaults to 'json'. Some providers (Linear, Slack) want
// 'form-urlencoded' for the token request.
tokenRequestContentType: 'form-urlencoded',
// Optional: defaults to true. Disable only if the provider rejects PKCE.
usePkce: false,
// Optional: extra query params on the authorize URL.
// authorizationParams: { prompt: 'consent' },
// Optional: provider's RFC 7009 token revocation endpoint, called on disconnect.
// revokeEndpoint: 'https://example.com/oauth/revoke',
},
// Optional: a logic function in this app to run right after a connection is
// established. See "Run a logic function on connect".
// onConnectLogicFunction: { universalIdentifier: '3a2b1c0d-...-...' },
// Optional: a logic function in this app to run right after a connection is
// removed. See "Run a logic function on disconnect".
// onDisconnectLogicFunction: { universalIdentifier: '4d5e6f70-...-...' },
});
```
```ts src/application.config.ts
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: '...',
displayName: 'Linear',
description: 'Connect Linear to Twenty.',
// OAuth client credentials live on the app registration (one OAuth app per
// Twenty server, configured by the admin) — not per-workspace. Declare them
// as serverVariables so the admin can fill them in once for all installs.
serverVariables: {
LINEAR_CLIENT_ID: {
description: 'OAuth client ID from your Linear OAuth application.',
isSecret: false,
isRequired: true,
},
LINEAR_CLIENT_SECRET: {
description: 'OAuth client secret from your Linear OAuth application.',
isSecret: true,
isRequired: true,
},
},
});
```
Pontos-chave:
* `name` é a string de identificador exclusivo usada em `listConnections({ providerName })` (kebab-case, deve corresponder a `^[a-z][a-z0-9-]*$`).
* `displayName` aparece na aba de configurações do app e na lista de ferramentas de IA.
* `clientIdVariable` / `clientSecretVariable` são **nomes**, não valores — devem corresponder às chaves declaradas em `defineApplication.serverVariables`. Os `client_id` e `client_secret` reais são inseridos pelo administrador do servidor por meio da interface de registro do app e nunca são versionados no seu repositório.
* Use `serverVariables` (não `applicationVariables`) — as credenciais OAuth são do servidor como um todo e há um app OAuth por servidor do Twenty.
* Até que ambos os `serverVariables` sejam preenchidos, a aba de configurações do app mostra uma dica "precisa de administrador do servidor" e o botão "Adicionar conexão" fica desativado.
* `type: 'oauth'` é o único valor compatível atualmente. O discriminador é compatível com versões futuras: tipos futuros (`'pat'`, `'api-key'`, ...) adicionarão novos blocos de subconfiguração ao lado de `oauth`.
O URL de callback do OAuth que seu provedor precisa adicionar à lista de permissões é:
```
https://<your-twenty-server>/auth/apps/callback
```
</Accordion>
<Accordion title="Execute uma função lógica ao conectar" description="Reaja no momento em que uma conexão é estabelecida">
Alguns provedores fornecem dados no momento da conexão que você precisa manter antes que a conexão possa ser usada — o exemplo clássico é o Slack, em que a resposta OAuth identifica o `team_id` do workspace pelo qual os eventos de entrada serão indexados. Defina `onConnectLogicFunction` para fazer referência a uma função lógica no mesmo app (pelo seu `universalIdentifier`), e ela será executada logo após o `ConnectedAccount` ser criado.
```ts src/connection-providers/slack-connection.ts
export default defineConnectionProvider({
universalIdentifier: '...',
name: 'slack',
displayName: 'Slack',
type: 'oauth',
oauth: {
/* ... */
},
// Runs claimSlackTeam after every successful Slack connection.
onConnectLogicFunction: {
universalIdentifier: '3a2b1c0d-1111-4222-8333-444455556666',
},
});
```
O hook é executado **de forma assíncrona no workspace que está se conectando** (ele é enfileirado, não aguardado), portanto um hook lento ou com falha nunca bloqueia ou quebra o callback OAuth — torne-o idempotente e faça com que ele mesmo gerencie suas próprias novas tentativas. O manipulador recebe:
```ts
type OnConnectPayload = {
connectionProviderId: string;
connectionProviderName: string; // e.g. 'slack'
connectedAccountId: string;
};
```
A partir daí, use `getConnection(connectedAccountId)` para ler o token de acesso atualizado e chamar a API do provedor (por exemplo, Slack `auth.test`) ou persistir um mapeamento com o [armazenamento de chave-valor](/l/pt/developers/extend/apps/logic/key-value-store).
</Accordion>
<Accordion title="Execute uma função lógica ao desconectar" description="Limpar quando uma conexão for removida">
Tudo o que um app reivindica no momento da conexão precisa ser liberado quando a conexão é encerrada. Uma integração do Slack que reivindica um `team_id` ao conectar, por exemplo, precisa liberar essa reivindicação para que outro espaço de trabalho possa conectar a mesma equipe do Slack. Defina `onDisconnectLogicFunction` para fazer referência a uma função lógica no mesmo app, e ela será executada logo após o `ConnectedAccount` ser excluído.
```ts src/connection-providers/slack-connection.ts
export default defineConnectionProvider({
universalIdentifier: '...',
name: 'slack',
displayName: 'Slack',
type: 'oauth',
oauth: {
/* ... */
},
// Runs releaseSlackTeam after every Slack disconnection.
onDisconnectLogicFunction: {
universalIdentifier: '4470aba8-5ff5-4800-88db-2a427cd8677c',
},
});
```
Assim como o hook de on-connect, ele é executado **assincronamente no espaço de trabalho que está sendo desconectado** e nunca bloqueia a desconexão. O handler recebe o mesmo formato de payload:
```ts
type OnDisconnectPayload = {
connectionProviderId: string;
connectionProviderName: string; // e.g. 'slack'
connectedAccountId: string;
};
```
O `ConnectedAccount` já não existe mais quando o hook é executado, então `getConnection(connectedAccountId)` não retorna mais resultado. Tudo o que a limpeza precisar (um `team_id`, um id de assinatura externa) deve ter sido gravado no [key-value store](/l/pt/developers/extend/apps/logic/key-value-store) no momento da conexão, indexado por `connectedAccountId`.
O hook é disparado quando uma conexão é removida por conta própria. A desinstalação do app remove suas conexões por meio de uma cascata de banco de dados; portanto, o hook não é executado nesse caso. Declare um `uninstallLogicFunction` em `defineApplication` para esse caminho: ele é executado antes de os metadados do app serem excluídos, então ainda pode chamar `listConnections` e limpar o que tiver restado.
</Accordion>
<Accordion title="listConnections / getConnection" description="Use conexões a partir de uma função de lógica">
Dentro de um handler de função de lógica, `listConnections({ providerName })` retorna as linhas `ConnectedAccount` deste app para o provedor fornecido, com tokens de acesso atualizados.
```ts src/logic-functions/handlers/create-linear-issue-handler.ts
import { listConnections } from 'twenty-sdk/logic-function';
export const createLinearIssueHandler = async (input: {
teamId?: string;
title?: string;
}) => {
if (!input.teamId || !input.title) {
return { success: false, error: 'teamId and title are required' };
}
const connections = await listConnections({ providerName: 'linear' });
// Workspace-shared credentials win when present; fall back to the first
// user-visibility one. For HTTP-route triggers you typically pick the
// request user's connection via event.userWorkspaceId instead.
const connection =
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
if (!connection) {
return {
success: false,
error:
'Linear is not connected. Open the app settings and click "Add connection".',
};
}
// Use connection.accessToken to call the third-party API.
const response = await fetch('https://api.linear.app/graphql', {
method: 'POST',
headers: {
Authorization: `Bearer ${connection.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`,
}),
});
return { success: response.ok };
};
```
Cada conexão tem:
| Campo | Descrição |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | ID de linha exclusivo; passe para `getConnection(id)` para buscar novamente um único registro |
| `visibility` | `'user'` (privada para um membro do workspace) ou `'workspace'` (compartilhada com todos os membros) |
| `scopes` | Permissões OAuth concedidas pelo provedor de origem (distintas de `visibility` — não têm relação) |
| `userWorkspaceId` | O id de userWorkspace do proprietário — útil para selecionar "a conexão do usuário da requisição" em gatilhos de rota HTTP |
| `accessToken` | Token de acesso OAuth recente (atualizado automaticamente se estiver expirado) |
| `nome` | O nome de exibição da conexão (derivado automaticamente no callback do OAuth, renomeável pelo usuário) |
| `ligar` | O e-mail da conta de upstream foi atualizado em cada reconexão; resolvido do OIDC `id_token` retornado na troca de token, retornando para o e-mail de 20 usuários quando o provedor não retornar um |
| `authFailedAt` | Definido quando a atualização mais recente falhou; o usuário deve reconectar |
Pontos-chave:
* Passe `{ providerName }` para filtrar por provedor; omita para obter todas as conexões que este app possui em todos os provedores.
* O servidor atualiza transparentemente o token de acesso antes de retornar. Seu handler sempre vê um token utilizável (ou `authFailedAt` definido).
* `getConnection(id)` é o equivalente de uma única linha.
</Accordion>
<Accordion title="Visibilidade por usuário vs. compartilhada no workspace" description="Como os usuários escolhem entre credenciais privadas e compartilhadas">
Quando um usuário clica em "Adicionar conexão", é solicitado que escolha uma visibilidade:
* **Apenas para mim** — a credencial é privada para o usuário que a conectou. Qualquer função de lógica chamada em seu nome (gatilho de rota HTTP com `isAuthRequired: true`) a vê; gatilhos cron e eventos de banco de dados não.
* **Compartilhada no workspace** — qualquer membro do workspace pode usar a credencial. Gatilhos de cron / banco de dados também a veem, pois não há um usuário da requisição.
Use a adequada para cada handler:
```ts
// HTTP-route trigger — prefer the request user's own connection.
const conn =
connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ??
connections.find((c) => c.visibility === 'workspace');
// Cron trigger — no request user; only shared credentials are sensible.
const conn = connections.find((c) => c.visibility === 'workspace');
```
Várias conexões por (usuário, provedor) são permitidas, então o mesmo usuário pode manter "Linear pessoal" e "Linear de trabalho" lado a lado.
</Accordion>
<Accordion title="Configuração única do provedor" description="Registre seu app OAuth no serviço de terceiros">
Para cada provedor de conexão, o administrador do servidor precisa primeiro registrar um app OAuth no serviço de terceiros.
1. Acesse as configurações de desenvolvedor do provedor (por exemplo, https://linear.app/settings/api/applications/new).
2. Defina a **URI de redirecionamento** como `\<SERVER_URL>/auth/apps/callback`.
3. Copie o **ID do cliente** e o **Segredo do cliente** gerados.
4. Abra o app instalado no Twenty como administrador do servidor → defina os valores nos `serverVariables` correspondentes.
5. Os membros do workspace podem então adicionar conexões na seção **Conexões** de cada app.
</Accordion>
</AccordionGroup>

View File

@@ -0,0 +1,110 @@
---
title: Armazenamento de chave-valor
description: Persista resultados intermediários, armazene em cache dados e compartilhe estado entre execuções de funções lógicas com o armazenamento interno de chave-valor do aplicativo.
icon: database
---
Funções de lógica são executadas de forma isolada em processos Node.js de curta duração — assim que uma execução termina, nada que foi mantido na memória sobrevive. Quando você precisa **lembrar de algo entre execuções** (armazenar em cache uma resposta cara de uma API, guardar um cursor para sincronizações incrementais, aplicar debounce ao trabalho ou passar estado de uma função para outra), persista isso no armazenamento interno de chave-valor.
Cada aplicativo recebe seu próprio namespace isolado: os registros são indexados pelo app autenticado, então suas chaves nunca podem colidir com — nem ser lidas por — outro aplicativo.
```text
┌─────────────────┐ kv.set(key, value) ┌──────────────────────────┐
│ Logic function │ ─────────────────────▶ │ Application KV store │
│ (your handler) │ ◀───────────────────── │ key (unique) │ value │
└─────────────────┘ kv.get(key) └──────────────────────────┘
```
## Obter, definir, excluir
Importe `kv` de `twenty-sdk/logic-function`. Os valores podem ser quaisquer valores serializáveis em JSON.
```ts src/logic-functions/sync-linear-issues.ts
import { kv } from 'twenty-sdk/logic-function';
// Read a value. Returns null when the key is missing.
const cursor = await kv.get<string>('sync-cursor:linear');
// Write a value. Creates the entry on first write, updates it afterwards.
await kv.set('sync-cursor:linear', newCursor);
// Delete an entry. Returns true when an entry was removed.
await kv.delete('sync-cursor:linear');
```
## Escopos
Cada registro tem um escopo, passado como uma opção em cada chamada. O padrão é `WORKSPACE`.
* **`WORKSPACE`** (padrão) — o registro é privado para a instalação atual do seu app no workspace. Cada workspace que instala o app recebe seu próprio conjunto independente de chaves. É isso que você quer para caches, cursores e estado por workspace.
* **`SERVER`** — o registro é compartilhado entre **todas as instalações** do seu app no servidor. Registros de servidor se comportam como **claims**: o valor armazenado é sempre o workspaceId que reivindicou a chave (omita `value` em `set` para reivindicar a chave para o workspace atual), e somente esse workspace pode sobrescrevê-la ou excluí-la. Qualquer instalação pode ler o registro.
Claims de servidor existem para roteamento entre workspaces. Um [server-route resolver](/l/pt/developers/extend/apps/logic/logic-functions#server-route-trigger) é executado no workspace proprietário do registro do aplicativo, mas um webhook de entrada normalmente só carrega um id de conta externa — não um workspaceId do Twenty. Faça com que cada workspace reivindique seu id externo no momento da conexão e, em seguida, resolva-o na rota:
```ts
// In the connected workspace, when the external account is linked:
await kv.set(`slack:team:${teamId}`, undefined, { scope: 'SERVER' });
// In the server-route resolver (owner workspace), on each webhook:
const workspaceId = await kv.get<string>(`slack:team:${teamId}`, {
scope: 'SERVER',
});
```
Como uma chave de servidor só pode ser reivindicada para o próprio workspace de quem chama e nunca sobrescrita por outro, um workspace não pode sequestrar um mapeamento que pertence a outra pessoa. `kv.set` lança uma exceção quando a chave já foi reivindicada por outro workspace.
## Use-o: armazenar em cache uma chamada cara
Um uso típico é armazenar em cache uma resposta lenta ou com limite de taxa de terceiros, para que execuções repetidas a reutilizem em vez de pagar o custo todas as vezes.
```ts src/logic-functions/getExchangeRate.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { kv } from 'twenty-sdk/logic-function';
const ONE_HOUR_MS = 60 * 60 * 1000;
type CachedRate = { rate: number; fetchedAt: number };
const handler = async (params: { from: string; to: string }) => {
const cacheKey = `cache:exchange-rate:${params.from}:${params.to}`;
const cached = await kv.get<CachedRate>(cacheKey);
if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) {
return { rate: cached.rate, cached: true };
}
const response = await fetch(
`https://api.example.com/rate?from=${params.from}&to=${params.to}`,
);
const { rate } = (await response.json()) as { rate: number };
await kv.set(cacheKey, { rate, fetchedAt: Date.now() });
return { rate, cached: false };
};
export default defineLogicFunction({
universalIdentifier: 'd9b2f4e6-1c83-4a07-9e52-6b1d3c8a0f47',
name: 'get-exchange-rate',
timeoutSeconds: 10,
handler,
});
```
## Padrões e dicas
* **Namespacing.** Prefixe chaves para manter diferentes responsabilidades separadas — `sync-cursor:linear`, `cache:exchange-rate:USD:EUR`, `lock:nightly-report`.
* **Expiração (TTL).** O armazenamento não possui expiração integrada. Armazene um carimbo de data/hora dentro do valor (como no exemplo de cache) e verifique-o na leitura, ou limpe chaves obsoletas a partir de uma [função acionada por cron](/l/pt/developers/extend/apps/logic/logic-functions).
* **O que armazenar.** Qualquer valor serializável em JSON — números, strings, arrays, objetos. Mantenha as entradas pequenas; isto é para coordenação e cache, não para blobs grandes ou arquivos. Para arquivos, use um campo `FILES` e [`uploadFile`](/l/pt/developers/extend/apps/logic/logic-functions#uploading-files).
* **Visibilidade.** Os registros ficam no banco de dados da instância, não como registros de workspace — eles nunca aparecem na interface do workspace, não fazem parte do modelo de dados do seu app e não precisam de permissões de função ou de objeto.
## Alternativa: um objeto de armazenamento consultável
O armazenamento interno é deliberadamente opaco: as entradas não são registros, então você não pode navegá-las na interface, relacioná-las a outros objetos ou filtrá-las com consultas de registros. Quando você precisar de qualquer uma dessas coisas — por exemplo, um log de sincronização visível ou estado por registro —, em vez disso defina um pequeno **objeto técnico** com um campo `key` exclusivo e um campo `value` `RAW_JSON`, e faça consultas por meio do [cliente de API tipado](/l/pt/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk). Veja [Objects](/l/pt/developers/extend/apps/data/objects) para a referência de `defineObject` e [Data → Unique indexes](/l/pt/developers/extend/apps/data/overview#unique-indexes) para impor exclusividade de chaves.
* **Definindo o escopo para um registro.** Adicione uma [relation](/l/pt/developers/extend/apps/data/relations) do objeto de armazenamento para o objeto de destino em vez de codificar o id na chave.
* **Visibilidade e permissões.** As linhas vivem no banco de dados do workspace como qualquer outro registro, portanto podem ser consultadas pela API e respeitam a [role](/l/pt/developers/extend/apps/config/roles) do seu app. Para manter o armazenamento fora da interface principal, deixe-o de fora do seu [menu de navegação](/l/pt/developers/extend/apps/layout/navigation-menu-items).
<Note>
Ao contrário do armazenamento interno, um objeto personalizado está sempre com escopo para um único workspace — ele não consegue compartilhar registros entre instalações como as chaves `SERVER` fazem.
</Note>

View File

@@ -0,0 +1,908 @@
---
title: Funções lógicas
description: Defina funções TypeScript no lado do servidor com gatilhos HTTP, cron e de eventos de banco de dados.
icon: bolt
---
As funções de lógica são funções TypeScript no lado do servidor que são executadas na plataforma Twenty. Elas podem ser acionadas por solicitações HTTP, agendamentos cron ou eventos de banco de dados — e também podem ser expostas como ferramentas para agentes de IA.
<AccordionGroup>
<Accordion title="defineLogicFunction" description="Defina funções de lógica e seus gatilhos">
Cada arquivo de função usa `defineLogicFunction()` para exportar uma configuração com um manipulador e gatilhos opcionais.
```ts src/logic-functions/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import type { RoutePayload } from 'twenty-sdk/logic-function';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (params: RoutePayload) => {
const client = new CoreApiClient();
const body = (params.body ?? {}) as { name?: string };
const name = body.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? '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: 'POST',
isAuthRequired: true,
},
/*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 código do app, prefixe o caminho da rota com `/s/` ao usar `RestApiClient`; a URL implantada usa a base injetada `TWENTY_FUNCTIONS_URL` (ou `\<server-url>/s` quando ela não está definida).
<Note>
Para invocar uma função de lógica acionada por rota a partir de um componente de front-end (headless), consulte [Chamando uma função de lógica](/l/pt/developers/extend/apps/layout/front-components#calling-a-logic-function).
</Note>
* **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.*`
* **serverRoute**: expõe uma única rota HTTP com escopo de registro. Uma função **resolver** (declarada com `serverRouteTriggerSettings`) é executada no workspace proprietário e retorna uma `Response` síncrona ou o workspace de destino E a função de lógica de destino para enfileirar; no caminho de enfileiramento, a plataforma confirma com `202` e executa esse **destino** na fila de workers. Veja [gatilho de rota de servidor](#server-route-trigger).
<Note>
Você também pode executar manualmente uma função usando a CLI:
```bash filename="Terminal"
yarn twenty dev:function:exec -n create-new-post-card -p '{"key": "value"}'
```
```bash filename="Terminal"
yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
```
Você pode acompanhar os logs com:
```bash filename="Terminal"
yarn twenty dev:function:logs
```
</Note>
#### 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](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html).
Importe o tipo `RoutePayload` de `twenty-sdk/logic-function`:
```ts
import type { RoutePayload } from 'twenty-sdk/logic-function';
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:
| Propriedade | Tipo | Descrição | Exemplo |
| ---------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- |
| `headers` | `Record\<string, string \| undefined>` | Cabeçalhos HTTP (apenas aqueles listados em `forwardedRequestHeaders`) | veja a seção abaixo |
| `queryStringParameters` | `Record\<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' }` |
| `pathParameters` | `Record\<string, string \| undefined>` | Parâmetros de caminho extraídos do padrão de rota | `/users/:id`, `/users/123` -> `{ id: '123' }` |
| `body` | `object \| null` | Corpo da requisição analisado (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
| `rawBody` | `string \| undefined` | Corpo da requisição UTF-8 original, antes da análise de JSON. Útil para verificar assinaturas de webhook no estilo HMAC (por exemplo, `X-Hub-Signature-256` do GitHub, Stripe). `undefined` quando o ambiente de execução não o preservou. | |
| `isBase64Encoded` | `boolean` | Se o corpo está codificado em base64 | |
| `requestContext.http.method` | `string` | Método HTTP (GET, POST, PUT, PATCH, DELETE) | |
| `requestContext.http.path` | `string` | Caminho 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`:
```ts
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 manipulador, acesse os cabeçalhos encaminhados assim:
```ts
const handler = async (event: RoutePayload) => {
const signature = event.headers['x-webhook-signature'];
const contentType = event.headers['content-type'];
// Validate webhook signature...
return { received: true };
};
```
<Note>
Os nomes dos cabeçalhos são normalizados para minúsculas. Acesse-os usando chaves em minúsculas (por exemplo, `event.headers['content-type']`).
</Note>
#### Resposta HTTP personalizada
Por padrão, retornar um valor simples do seu handler o envia de volta como uma resposta `200` (JSON para objetos, `text/plain` para strings). Para controlar o código de status e os cabeçalhos da resposta, retorne um `Response` de `twenty-sdk/logic-function`:
```ts
import { Response } from 'twenty-sdk/logic-function';
const handler = async (event: RoutePayload) => {
return new Response('<h1>Hello</h1>', {
status: 201,
headers: { 'content-type': 'text/html' },
});
};
```
Por motivos de segurança, os cabeçalhos de resposta são restringidos a uma lista de permissões. Qualquer cabeçalho que não esteja na lista (por exemplo, `Set-Cookie`, cabeçalhos CORS como `Access-Control-Allow-Origin` ou cabeçalhos personalizados `X-*`) é silenciosamente descartado antes de a resposta ser enviada. Os cabeçalhos de resposta permitidos são:
* `content-type`
* `content-language`
* `content-disposition`
* `cache-control`
* `retry-after`
<Note>
O código de status deve ser um código de status HTTP válido (entre 100 e 599). Os nomes dos cabeçalhos de resposta são comparados sem distinção entre maiúsculas e minúsculas.
</Note>
#### Respostas de erro da plataforma
Além das próprias respostas do seu handler, a plataforma responde diretamente às chamadas de rota em algumas situações: `404` quando a rota ou função não existe, `403` quando a aplicação está parada, `429` quando o limite de taxa de execução é atingido e `422` quando as `dependencies` de produção da aplicação são grandes demais para serem instaladas — consulte [limites de tamanho de dependencies](/l/pt/developers/extend/apps/getting-started/project-structure#dependencies).
#### Gatilho de rota de servidor
`httpRouteTriggerSettings` expõe uma função em `/s/` e resolve o workspace a partir do host da solicitação — o que funciona quando cada workspace tem seu próprio domínio. Provedores de terceiros, entretanto, entregam os eventos de todos os workspaces para **uma** URL. Para esse caso, use `serverRouteTriggerSettings`.
Nesse caso, o gatilho tem duas partes:
1. Uma função de lógica de **resolver** — declarada com `serverRouteTriggerSettings` — é executada no seu **workspace proprietário** (o workspace que é proprietário do registro da aplicação). Ela inspeciona a requisição recebida e retorna um dos seguintes:
* `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }` — a plataforma coloca esse destino em fila no workspace resolvido e confirma com `202 { queued: true }`, ou
* uma `Response` de `twenty-sdk/logic-function` — a plataforma repete essa resposta HTTP **sincronamente** e **não** coloca nenhum destino em fila (use isso para handshakes de desafio, como o `url_verification` do Slack).
O resolver é o ponto único de autorização — a URL carrega apenas o identificador do resolver. **Este é o local preferencial para verificar assinaturas de requisição**: o resolver é executado antes de qualquer efeito colateral, tem acesso ao `rawBody` original e aos headers encaminhados e pode rejeitar sem nunca tocar no alvo.
2. Uma função de lógica de **target** — uma função de lógica regular por workspace — então é executada no workspace resolvido com o payload retornado pelo resolver (ou o payload original da requisição, se o resolver não o tiver transformado). Seu valor de retorno **não** é observado pelo chamador HTTP quando o resolver escolheu o caminho de enfileiramento.
```ts src/logic-functions/resolve-server-route.logic-function.ts
import { createHmac, timingSafeEqual } from 'crypto';
import { defineLogicFunction } from 'twenty-sdk/define';
import { Response, type RoutePayload } from 'twenty-sdk/logic-function';
// Runs in the owner workspace. Verifies the request signature, picks
// which target function should handle the event, and returns the
// workspace + target the platform should dispatch to.
const handler = async (event: RoutePayload) => {
// Fail closed if the secret isn't configured — never fall back to an
// empty key, which would let any caller forge a matching signature.
const secret = process.env.GITHUB_WEBHOOK_SECRET;
if (!secret) {
throw new Error('GITHUB_WEBHOOK_SECRET is not configured');
}
const signature = event.headers['x-hub-signature-256'] ?? '';
const expected =
'sha256=' +
createHmac('sha256', secret).update(event.rawBody ?? '').digest('hex');
const a = Buffer.from(signature);
const b = Buffer.from(expected);
if (a.length !== b.length || !timingSafeEqual(a, b)) {
throw new Error('invalid signature');
}
const body = (event.body ?? {}) as {
challenge?: string;
metadata?: { twentyWorkspaceId?: string };
type?: string;
};
// Handshakes must be answered on this same response, so reply from the
// resolver instead of returning a dispatch target.
if (body.type === 'url_verification') {
return new Response({ challenge: body.challenge });
}
const workspaceId = body.metadata?.twentyWorkspaceId;
if (!workspaceId) {
throw new Error('event is not linked to a workspace');
}
return {
workspaceId,
// Route different event types to different target functions.
targetLogicFunctionUniversalIdentifier:
body.type === 'invoice.paid'
? 'c4e2a9b1-7d4e-4c9a-9f2b-2e1d6a4c8e10' // handle-invoice-paid
: 'd5f3b0c2-8e5f-5d0b-a0c3-3f2e7b5d9f21', // handle-other-event
};
};
export default defineLogicFunction({
universalIdentifier: 'b3c2f0a1-7d4e-4c9a-9f2b-2e1d6a4c8e10',
name: 'resolve-server-route',
handler,
serverRouteTriggerSettings: {
forwardedRequestHeaders: ['x-hub-signature-256'],
},
});
```
```ts src/logic-functions/handle-invoice-paid.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import type { RoutePayload } from 'twenty-sdk/logic-function';
// Runs in the resolved workspace. The resolver has already authenticated
// the request, so this handler can focus on the actual work.
const handler = async (event: RoutePayload) => {
// ...handle the verified event
return { received: true };
};
export default defineLogicFunction({
universalIdentifier: 'c4e2a9b1-7d4e-4c9a-9f2b-2e1d6a4c8e10',
name: 'handle-invoice-paid',
handler,
});
```
O endpoint fica acessível em:
```
POST https://your-twenty-server.com/webhooks/server/:resolverLogicFunctionUniversalIdentifier
```
O identificador é o `universalIdentifier` do resolver, vindo do seu manifest. Registre essa URL junto ao provedor.
**Respondendo a uma verificação GET.** Alguns provedores verificam um endpoint antes de enviar dados a ele, enviando um `GET` com um desafio para a mesma URL à qual posteriormente enviarão eventos por `POST` — a API do WhatsApp Cloud da Meta é um deles. Uma rota do servidor responde somente a `POST`, a menos que você indique o contrário, portanto declare ambos os métodos:
```ts
serverRouteTriggerSettings: {
forwardedRequestHeaders: ['x-hub-signature-256'],
httpMethods: ['GET', 'POST'],
}
```
O desafio chega em `event.queryStringParameters`, e retornar uma `Response` o repete para o provedor na mesma solicitação. Um corpo de string é enviado como `text/plain`, que é o que esses provedores esperam:
```ts
const handler = async (event: RoutePayload) => {
if (event.requestContext.http.method === 'GET') {
return new Response(event.queryStringParameters['hub.challenge']);
}
// …handle the delivered event
};
```
`httpMethods` substitui o padrão em vez de ser adicionado a ele; portanto, apenas `['GET']` faz com que a rota rejeite `POST`. Apenas `GET` e `POST` são compatíveis. Deixe-o não definido, a menos que o provedor precise do segundo verbo: uma rota que declara `GET` terá seu resolvedor executado por qualquer chamador não autenticado, incluindo os rastreadores e expansores de links que enviam `GET` sem solicitação. Qualquer coisa para a qual a plataforma não tenha um método recebe a resposta `405` sem que o resolvedor seja executado.
<Note>
**O aplicativo deve ser reivindicado e instalado em seu workspace proprietário.** Como o resolvedor é executado no **workspace proprietário** (o workspace que é proprietário do registro do aplicativo), um acionador de rota de servidor só funciona depois que o aplicativo tiver sido *reivindicado* — ou seja, tiver um workspace proprietário — **e** esse aplicativo estiver **instalado no workspace proprietário**. Até que ambas as condições sejam verdadeiras, o resolvedor não tem onde ser executado, portanto a rota não pode ser despachada. Um aplicativo que expõe uma função lógica `serverRouteTriggerSettings`, portanto, não pode ser listado no marketplace até que seja reivindicado e instalado em seu workspace proprietário.
</Note>
**Contrato do resolver.** O tipo `LogicFunctionConfig` do SDK aplica isso em tempo de compilação: assim que você define `serverRouteTriggerSettings`, o seu handler fica limitado a retornar uma `Response` ou `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }` (ou uma `Promise` de qualquer um deles). No caminho de dispatch, o `workspaceId` deve ser um workspace onde a função de destino esteja instalada, caso contrário a requisição é rejeitada com `404`. Um resultado que não corresponda a nenhum dos formatos — incluindo um cujos identificadores não sejam UUIDs — é rejeitado com `502`.
| Campo | Tipo | Notas |
| ---------------------------------------- | ------------------- | --------------------------------------------------------------------------- |
| `workspaceId` | `string` | UUID do workspace onde o alvo será executado. |
| `targetLogicFunctionUniversalIdentifier` | `string` | `universalIdentifier` da função de lógica a ser invocada naquele workspace. |
| `payload` | `object` (opcional) | Se definido, substitui o corpo da requisição enviado ao alvo. |
<Warning>
**A verificação de assinatura é sua responsabilidade — verifique no resolver.** A plataforma não verifica assinaturas de requisição. O resolver é o local recomendado para fazer isso: ele é executado primeiro, com acesso a `event.rawBody` e aos headers que você listou em `forwardedRequestHeaders`, e um erro lançado (ou qualquer `workspaceId` que não corresponda) interrompe o despacho antes que o alvo seja invocado. Se, em vez disso, você empurrar a verificação para o target, o target deve ter cuidado para não perder o `rawBody` e os headers — isto é, o resolver não deve retornar um `payload`. Sempre verifique **antes** de qualquer efeito colateral e use uma comparação em tempo constante.
</Warning>
Para assinaturas de requisição, a maioria dos provedores assina com HMAC-SHA256; as partes que diferem são o nome do header, a codificação do digest e a string de payload assinada. Alguns exemplos:
| Provedor | Headers a encaminhar | String assinada | Digest |
| ---------------------------- | ------------------------------------------------------ | ---------------------------- | ------------------------------------------------------- |
| Svix (Recall, Resend, Clerk) | `webhook-id`, `webhook-timestamp`, `webhook-signature` | `{id}.{timestamp}.{rawBody}` | base64 (o segredo está em base64 após remover `whsec_`) |
| Stripe | `stripe-signature` | `{timestamp}.{rawBody}` | hex |
| GitHub | `x-hub-signature-256` | `{rawBody}` | hex (prefixado com `sha256=`) |
| Shopify | `x-shopify-hmac-sha256` | `{rawBody}` | base64 |
| Slack | `x-slack-signature`, `x-slack-request-timestamp` | `v0:{timestamp}:{rawBody}` | hex (prefixado com `v0=`) |
O exemplo de resolver acima já mostra o fluxo de HMAC-SHA256 do GitHub — adapte o nome do header, a codificação do digest e a string de payload assinada de acordo com o provedor com o qual você está integrando.
<Note>
Quando o resolver retorna um objeto de dispatch, a rota responde com `202 { queued: true }` e o destino é executado na fila de workers — quem faz a chamada nunca observa a latência, o resultado ou as falhas do destino (essas são registradas nos logs de execução). Isso evita que reentregas do remetente amplifiquem a lentidão do processamento, que é o que você quer para a ingestão de webhooks.
Quando o chamador precisa ler o corpo da resposta na mesma requisição (handshakes de desafio, acknowledgements interativos), retorne, em vez disso, uma `Response` a partir do **resolver**. A plataforma o replica de forma síncrona e ignora a fila; seus headers passam pela mesma allow-list que as respostas de rotas HTTP. Mantenha o resolver rápido — alguns provedores (por exemplo, Slack) atingem timeout em poucos segundos. Como o resolver fica acessível como um endpoint público, proteja-o com rate limiting na sua borda.
</Note>
#### Payload do gatilho de evento do banco de dados
Quando um gatilho de evento do banco de dados invoca sua função lógica, ela recebe um `DatabaseEventPayload` por registro alterado. O payload combina metadados sobre o workspace e o objeto de origem com o evento em nível de registro.
```ts
import type {
DatabaseEventPayload,
ObjectRecordCreateEvent,
ObjectRecordDestroyEvent,
ObjectRecordUpdateEvent,
} from 'twenty-sdk/logic-function';
type Person = {
id: string;
emails?: { primaryEmail?: string };
};
```
O payload inclui:
| Propriedade | Descrição |
| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |
| `name` | Nome do evento, como `person.updated`. |
| `workspaceId` | Workspace onde o evento aconteceu. |
| `objectMetadata` | Metadados do objeto que foi alterado. |
| `recordId` | ID do registro alterado. |
| `userId`, `userWorkspaceId`, `workspaceMemberId` | Campos do autor quando o evento foi causado por um usuário do workspace. |
| `properties` | Dados do registro para o evento, com `before`, `after`, `diff` e `updatedFields`, dependendo da operação. |
| Evento | Dados do registro |
| ------------------ | -------------------------------------------------------------------------------------------------------------- |
| `person.created` | `event.properties.after` |
| `person.updated` | `event.properties.before`, `event.properties.after`, `event.properties.diff`, `event.properties.updatedFields` |
| `person.destroyed` | `event.properties.before` |
Para exclusões lógicas, `.deleted` segue o formato de atualização porque o campo `deletedAt` do registro é alterado.
Para exclusões permanentes, use `.destroyed`.
<Note>
`databaseEventTriggerSettings.updatedFields` filtra quais eventos de atualização disparam a função.
`event.properties.updatedFields` informa quais campos realmente foram alterados no evento atual.
</Note>
Exemplo de evento de criação:
```ts
type PersonCreatedEvent = DatabaseEventPayload<
ObjectRecordCreateEvent<Person>
>;
const handler = async (event: PersonCreatedEvent) => {
const person = event.properties.after;
return {
personId: event.recordId,
email: person.emails?.primaryEmail,
};
};
```
Exemplo de evento de atualização:
```ts
type PersonUpdatedEvent = DatabaseEventPayload<
ObjectRecordUpdateEvent<Person>
>;
const handler = async (event: PersonUpdatedEvent) => {
const { before, after, diff, updatedFields } = event.properties;
return {
personId: event.recordId,
updatedFields,
previousEmail: before.emails?.primaryEmail,
currentEmail: after.emails?.primaryEmail,
emailDiff: diff.emails,
};
};
```
Disparar somente em atualizações de email:
```ts
export default defineLogicFunction({
...,
databaseEventTriggerSettings: {
eventName: 'person.updated',
updatedFields: ['emails'],
},
});
```
Exemplo de evento de destruição:
```ts
type PersonDestroyedEvent = DatabaseEventPayload<
ObjectRecordDestroyEvent<Person>
>;
const handler = async (event: PersonDestroyedEvent) => {
const personBeforeDestroy = event.properties.before;
return {
personId: event.recordId,
email: personBeforeDestroy.emails?.primaryEmail,
};
};
```
#### Contexto de execução
Cada manipulador recebe um segundo argumento que descreve a própria execução, independentemente do que a acionou. Enquanto o primeiro argumento muda de formato conforme o acionador, este não muda:
```ts
import type { LogicFunctionExecutionContext } from 'twenty-sdk/logic-function';
const handler = async (
event: DatabaseEventPayload,
{ workspaceId, workspaceMemberId }: LogicFunctionExecutionContext,
) => {
if (workspaceMemberId === null) {
throw new Error('This function must be triggered by a person');
}
return { reviewedBy: workspaceMemberId, workspaceId };
};
```
| Campo | Tipo | Descrição |
| ------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------- |
| `workspaceId` | `string` | O espaço de trabalho ao qual a execução pertence. |
| `userWorkspaceId` | `string \| null` | A pessoa que acionou a execução, como seu vínculo neste espaço de trabalho. |
| `workspaceMemberId` | `string \| null` | A mesma pessoa como um registro de membro do espaço de trabalho, ao qual você vincula registros. |
| `retryCount` | `number` | `0` na primeira tentativa. Consulte [Tarefas em segundo plano](/l/pt/developers/extend/apps/logic/background-jobs). |
| `maxRetries` | `number` | Limite superior de novas tentativas solicitadas pelo aplicativo. |
`userWorkspaceId` e `workspaceMemberId` são `null` quando ninguém acionou a execução: agendamentos cron, hooks de instalação e webhooks não autenticados não têm uma pessoa por trás deles. Eles também são `null` quando a pessoa não tem um registro de membro do espaço de trabalho ou quando o seu foi excluído.
<Note>
**O contexto informa quem acionou a execução.** O que a execução pode *fazer* é uma declaração separada, abaixo.
</Note>
#### O acesso que uma chamada usa
Cada cliente — `CoreApiClient`, `MetadataApiClient` e `RestApiClient` — atua como a pessoa que acionou a execução: seu papel é combinado com o do seu aplicativo, portanto a chamada nunca pode fazer mais do que qualquer um dos dois. Esse é o padrão e significa que uma pessoa nunca pode usar seu aplicativo para exceder as próprias permissões.
Quando ninguém acionou a execução, não há uma pessoa para representar, então o mesmo cliente recorre ao acesso do próprio aplicativo: agendamentos cron, hooks de instalação e webhooks não autenticados não exigem tratamento especial.
Algumas chamadas precisam legitimamente do acesso do próprio aplicativo mesmo quando há uma pessoa por trás da execução — para ler os registros de configuração do seu aplicativo ou realizar tarefas que essa pessoa não conseguiria realizar por conta própria. Crie um segundo cliente para essas chamadas:
```ts
import { CoreApiClient } from 'twenty-client-sdk/core';
const asPerson = new CoreApiClient();
const asApplication = new CoreApiClient({ runAs: 'application' });
const handler = async (event: DatabaseEventPayload) => {
// config records only your app may read
const { slackUserLinks } = await asApplication.query({ ... });
// created as the person, so it is attributed and permission-checked
await asPerson.mutation({ createNote: { ... } });
};
```
Crie ambos uma vez, no escopo do módulo, e cada local de chamada indicará qual acesso usa pelo cliente que chama. `RestApiClient` aceita a mesma opção:
```ts
import { RestApiClient } from 'twenty-client-sdk/rest';
const asApplication = new RestApiClient({ runAs: 'application' });
```
<Note>
**Uma execução que ninguém acionou atua como seu aplicativo.** Agendamentos cron, hooks de instalação e webhooks não autenticados não têm uma pessoa por trás deles, portanto o cliente padrão recorre ao acesso do próprio aplicativo e continua funcionando. `runAs: 'application'` é necessário apenas quando você quer esse acesso em uma execução que uma pessoa *acionou*.
Verifique `context.workspaceMemberId` quando uma função se comportar de modo diferente dependendo de haver alguém por trás dela, por exemplo, para atribuir um registro.
</Note>
Os auxiliares do SDK que acessam os recursos do próprio aplicativo sempre usam o acesso dele e ignoram `runAs`: o [armazenamento de chave-valor](/l/pt/developers/extend/apps/logic/key-value-store), as [conexões](/l/pt/developers/extend/apps/logic/connections), `runAgent`, `getPublicAssetUrl` e a cobrança de créditos.
#### Expor uma função como ferramenta de IA ou como ação de fluxo de trabalho
As funções de lógica podem ser expostas em duas superfícies, cada uma com seu próprio gatilho:
* **`toolTriggerSettings`** — torna a função disponível para os recursos de IA do Twenty (chat, MCP, chamadas de função). Usa o JSON Schema padrão, o formato que os LLMs entendem nativamente.
* **`workflowActionTriggerSettings`** — torna a função visível como uma etapa no construtor visual de fluxos de trabalho. Usa o `InputSchema` avançado do Twenty para que o construtor possa renderizar editores de campo adequados, seletores de variáveis e rótulos.
Uma função pode optar por uma, pela outra ou por ambas. Ficam ao lado de `cronTriggerSettings`, `databaseEventTriggerSettings` e `httpRouteTriggerSettings` — mesmo padrão, mesmo formato.
<Note>
**Relação com a ação Code do fluxo de trabalho.** A ação **Code** incorporada no construtor de fluxos de trabalho é, em si, uma função lógica — a Twenty cria uma para cada etapa Code e exibe seu editor em linha. `workflowActionTriggerSettings` é como você transforma esse código em linha único em uma ação **reutilizável**: defina a função uma vez no seu app e ela se torna selecionável em qualquer fluxo de trabalho, em vez de ser copiada e colada em cada etapa Code. Veja a [ação Code](/l/pt/user-guide/workflows/capabilities/workflow-actions#code) no guia do usuário para a visão do usuário final.
</Note>
```ts src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
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,
toolTriggerSettings: {},
});
```
Pontos-chave:
* Uma função pode misturar superfícies — declare tanto `toolTriggerSettings` quanto `workflowActionTriggerSettings` para expô-la no chat E no construtor de fluxos de trabalho.
* `toolTriggerSettings.inputSchema` e `workflowActionTriggerSettings.inputSchema` são opcionais. Quando omitidos, o construtor de manifestos os infere a partir do código-fonte do handler (JSON Schema para a ferramenta de IA, `InputSchema` do Twenty para a ação de fluxo de trabalho). Forneça um explicitamente quando quiser uma tipagem mais rica — por exemplo, com campos compatíveis com `FieldMetadataType`, como `CURRENCY` ou `RELATION` para o construtor de fluxos de trabalho, ou com campos `description` que o agente de IA pode ler:
```ts
export default defineLogicFunction({
...,
toolTriggerSettings: {
inputSchema: {
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'],
},
},
});
```
Para declarar seus parâmetros **uma vez** e atender a ambas as interfaces, defina um único JSON Schema (`InputJsonSchema`) e converta-o para a ação de fluxo de trabalho com `jsonSchemaToInputSchema` de `twenty-sdk/logic-function`. `toolTriggerSettings.inputSchema` recebe o JSON Schema diretamente, enquanto `workflowActionTriggerSettings.inputSchema` espera o `InputSchema` da Twenty:
```ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { jsonSchemaToInputSchema, type InputJsonSchema } from 'twenty-sdk/logic-function';
const inputSchema: InputJsonSchema = {
type: 'object',
properties: {
companyName: { type: 'string', label: 'Company name' },
domain: { type: 'string', label: 'Domain' },
},
required: ['companyName'],
};
export default defineLogicFunction({
...,
toolTriggerSettings: { inputSchema },
workflowActionTriggerSettings: {
label: 'Enrich Company',
icon: 'IconBuilding',
inputSchema: jsonSchemaToInputSchema(inputSchema),
},
});
```
##### Um exemplo completo de ação de fluxo de trabalho
`workflowActionTriggerSettings` aceita quatro campos:
| Campo | Finalidade |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `label` | Nome exibido para a ação no seletor de etapas do construtor de fluxos de trabalho. O padrão é o `name` da função. |
| `icon` | Ícone exibido ao lado da ação (um nome de `tabler-icons`, por exemplo `IconBuilding`). |
| `inputSchema` | O rico `InputSchema` da Twenty — o que o construtor renderiza como campos configuráveis (com seletores de variáveis). Opcional; inferido a partir do handler quando omitido. |
| `outputSchema` | Declara o formato que o handler retorna, para que **as etapas subsequentes possam mapear para seus campos de saída**. Opcional; sem isso, a saída é exposta como um único valor opaco. |
Juntando tudo — uma função exposta como uma ação de fluxo de trabalho, com uma saída declarada para que etapas posteriores possam referenciar `taskId`:
```ts src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { jsonSchemaToInputSchema, type InputJsonSchema } from 'twenty-sdk/logic-function';
import { CoreApiClient } from 'twenty-client-sdk/core';
const inputSchema: InputJsonSchema = {
type: 'object',
properties: {
companyName: { type: 'string', label: 'Company name' },
domain: { type: 'string', label: 'Domain' },
},
required: ['companyName'],
};
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,
},
});
// The keys returned here should match the `outputSchema` properties below.
return { taskId: result.createTask.id, enriched: true };
};
export default defineLogicFunction({
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
name: 'enrich-company',
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
workflowActionTriggerSettings: {
label: 'Enrich Company',
icon: 'IconBuilding',
inputSchema: jsonSchemaToInputSchema(inputSchema),
outputSchema: [
{
type: 'object',
properties: {
taskId: { type: 'string' },
enriched: { type: 'boolean' },
},
},
],
},
});
```
Depois que o app é instalado, **Enrich Company** aparece no seletor de ações do construtor de fluxos de trabalho. O construtor renderiza `companyName` e `domain` como campos de entrada (cada um podendo extrair valores de etapas anteriores), e as etapas subsequentes podem referenciar as saídas `taskId` e `enriched` da etapa.
<Note>
**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.
</Note>
</Accordion>
</AccordionGroup>
<Note>
**Auxiliares de tempo de execução.** `twenty-sdk/utils` reexporta pequenos auxiliares de tempo de execução para que os handlers nunca importem diretamente de `twenty-shared`. Por exemplo, `isDefined(value)` retorna `false` tanto para `null` quanto para `undefined` — use-o para restringir com segurança entradas opcionais de handlers, que podem chegar como `null` em tempo de execução mesmo quando tipadas como `T | undefined`:
```ts
import { isDefined } from 'twenty-sdk/utils';
const handler = async (params: { parentMessageId?: string }) => {
if (isDefined(params.parentMessageId)) {
// params.parentMessageId is narrowed to string here
}
};
```
</Note>
<Note>
**Hooks de instalação** — manipuladores de pré-instalação, pós-instalação e desinstalação — compartilham esse ambiente de execução, mas são declarados com suas próprias funções de definição e não usam configurações de gatilho. Veja [Hooks de instalação](/l/pt/developers/extend/apps/config/install-hooks) para `definePreInstallLogicFunction`, `definePostInstallLogicFunction` e `defineUninstallLogicFunction`.
</Note>
## Criar uma atividade da linha do tempo.
Use `createTimelineActivity()` para publicar um evento de domínio explícito a partir de uma função lógica. Primeiro, defina o evento como um [tipo de atividade da linha do tempo](/l/pt/developers/extend/apps/data/timeline-activity-types) e, em seguida, enderece o tipo e os objetos por seus identificadores universais estáveis:
```ts
import { createTimelineActivity } from 'twenty-sdk/logic-function';
await createTimelineActivity({
timelineActivityTypeUniversalIdentifier:
'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
targetObjectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
targetRecordId: postCardId,
happensAt: new Date().toISOString(),
properties: {
deliveryProvider: 'postal-service',
},
});
```
O Twenty resolve os identificadores universais para os IDs de metadados específicos da instalação, valida se o tipo de atividade da linha do tempo pertence ao app chamador e cria um instantâneo de seus metadados de apresentação na nova atividade.
As entradas obrigatórias são `timelineActivityTypeUniversalIdentifier`, `targetObjectUniversalIdentifier` e `targetRecordId`. Você também pode fornecer `happensAt`, `properties` e `workspaceMemberId`. `happensAt` controla a posição e o horário exibido do evento na linha do tempo; por padrão, usa o horário de criação.
Para associar outro registro ao evento, forneça `linkedRecordId` e `linkedObjectMetadataUniversalIdentifier` juntos. Você também pode fornecer `linkedRecordCachedName` como uma alternativa histórica de exibição:
```ts
await createTimelineActivity({
timelineActivityTypeUniversalIdentifier:
'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
targetObjectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
targetRecordId: postCardId,
linkedObjectMetadataUniversalIdentifier: RECIPIENT_UNIVERSAL_IDENTIFIER,
linkedRecordId: recipientId,
linkedRecordCachedName: recipientName,
});
```
A role da função lógica precisa de permissão de escrita no objeto padrão `timelineActivity`. Mantenha os tipos de evento explícitos não vinculados a uma `action`; um tipo vinculado a uma ação já recebe eventos de auditoria automáticos e, caso contrário, produziria linhas duplicadas.
## 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.
| Cliente | Importar | Endpoint | Gerado? |
| ------------------- | ---------------------------- | -------------------------------------------------------------------- | -------------------------- |
| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — dados do espaço de trabalho (registros, objetos) | Sim, em tempo de dev/build |
| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — configuração do espaço de trabalho, upload de arquivos | Não, vem pré-compilado |
<AccordionGroup>
<Accordion title="CoreApiClient" description="Consultar e modificar dados do espaço de trabalho (registros, objetos)">
`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 dev:build`, então é totalmente tipado para corresponder aos seus objetos e campos.
```ts
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.
<Note>
**CoreApiClient é gerado em tempo de dev/build.** Se você usá-lo sem executar primeiro `yarn twenty dev` ou `yarn twenty dev: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`.
</Note>
#### 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:
```ts
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);
```
</Accordion>
<Accordion title="MetadataApiClient" description="Configuração do espaço de trabalho, aplicativos e upload de arquivos">
`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. Ele aceita a mesma opção `runAs` que `CoreApiClient` — consulte [O acesso que uma chamada usa](#whose-access-a-call-uses).
```ts
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:
```ts
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âmetro | Tipo | Descrição |
| ---------------------------------- | -------- | ----------------------------------------------------------------- |
| `fileBuffer` | `Buffer` | O conteúdo bruto do arquivo |
| `filename` | `string` | O nome do arquivo (usado para armazenamento e exibição) |
| `contentType` | `string` | Tipo MIME (padrão para `application/octet-stream` se omitido) |
| `fieldMetadataUniversalIdentifier` | `string` | O `universalIdentifier` do campo do tipo de 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 aplicativo esteja instalado.
* A `url` retornada é uma URL assinada que você pode usar para acessar o arquivo enviado.
</Accordion>
</AccordionGroup>
<Note>
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 para o acesso padrão: o papel de uma pessoa combinado com o do seu aplicativo quando há alguém por trás da execução, o papel do próprio aplicativo quando não há ninguém. A pessoa é quem acionou a execução em uma função lógica ou quem está visualizando a página em um componente front.
* `TWENTY_APP_APPLICATION_ACCESS_TOKEN` — Chave de curta duração com escopo apenas para o papel do próprio aplicativo. Apenas funções lógicas, sempre injetada nelas, e é o que `runAs: 'application'` usa.
Você **não** precisa passá-las aos clientes — eles leem de `process.env` automaticamente, e [O acesso que uma chamada usa](#whose-access-a-call-uses) explica como escolher entre elas. As permissões do próprio aplicativo são determinadas pelo papel declarado com `defineApplicationRole()` (ou referenciado via `defaultRoleUniversalIdentifier` em `application-config.ts`); uma execução que atua como uma pessoa nunca pode exceder esse papel nem o dela.
</Note>

View File

@@ -0,0 +1,61 @@
---
title: Visão Geral
description: TypeScript do lado do servidor que é executado dentro do Twenty — acionado por rotas HTTP, agendamentos cron, eventos de banco de dados, ferramentas de IA ou ações de fluxos de trabalho.
icon: bolt
---
A **camada de lógica** de um app do Twenty é o código que *é executado* — manipuladores TypeScript do lado do servidor que reagem a solicitações HTTP, agendamentos CRON e alterações de registros; habilidades e agentes de IA que vivem dentro do workspace; e conexões OAuth que permitem que suas funções ajam em nome de um usuário em serviços de terceiros.
```text
┌─ HTTP route ──┐
│ Cron schedule │
│ Database event │ ┌────────────────────┐
triggers ─┤ AI tool call ├─────▶│ Logic function │
│ Workflow action │ │ (your handler) │
│ Manual exec │ └────────────────────┘
└────────────────────┘ │
▼
┌────────────────────────────┐
│ Twenty API (records) │
│ Third-party API │
│ (via Connection token) │
└────────────────────────────┘
```
## Nesta seção
<CardGroup cols={2}>
<Card title="Funções lógicas" icon="bolt" href="/l/pt/developers/extend/apps/logic/logic-functions">
O bloco de construção principal — tipos de gatilho, payloads e o cliente de API tipado.
</Card>
<Card title="Habilidades e agentes" icon="robot" href="/l/pt/developers/extend/apps/logic/skills-and-agents">
Instruções reutilizáveis para agentes de IA e assistentes com prompts de sistema personalizados.
</Card>
<Card title="Conexões" icon="plug" href="/l/pt/developers/extend/apps/logic/connections">
Credenciais OAuth que seu app mantém para serviços de terceiros — Linear, GitHub, Slack e outros.
</Card>
<Card title="Armazenamento de chave-valor" icon="database" href="/l/pt/developers/extend/apps/logic/key-value-store">
Persistir o estado entre execuções de funções de lógica — caches, cursores e declarações entre espaços de trabalho.
</Card>
<Card title="Tarefas em segundo plano" icon="layer-group" href="/l/pt/developers/extend/apps/logic/background-jobs">
Adicione à fila a execução de uma função de lógica nos workers para contornar o limite de tempo de cada execução.
</Card>
</CardGroup>
## Tipos de gatilho em resumo
Uma função de lógica escolhe um ou mais gatilhos — cada entrada abaixo é um campo separado em `defineLogicFunction()`:
| Disparador | Quando é executado | Configuração |
| ----------------------------- | ----------------------------------------------------------------- | ------------------------------- |
| **Rota HTTP** | Uma solicitação atinge seu endpoint `/s/\<path>` | `httpRouteTriggerSettings` |
| **Cron** | Uma expressão CRON corresponde | `cronTriggerSettings` |
| **Evento de banco de dados** | Um registro do workspace é criado, atualizado ou excluído | `databaseEventTriggerSettings` |
| **Ferramenta de IA** | Um recurso de IA do Twenty decide chamar sua função | `toolTriggerSettings` |
| **Ação de fluxo de trabalho** | Uma etapa de fluxo de trabalho invoca sua função | `workflowActionTriggerSettings` |
As funções são executadas em sandbox, em processos Node.js isolados, e acessam o workspace por meio de um cliente de API tipado, com escopo definido pelo papel declarado em [`defineApplication()`](/l/pt/developers/extend/apps/config/application).
<Note>
**Ganchos de instalação** — código que é executado antes ou depois da instalação — compartilham esse runtime, mas usam suas próprias funções define e ficam em [Config → Install Hooks](/l/pt/developers/extend/apps/config/install-hooks).
</Note>

View File

@@ -0,0 +1,193 @@
---
title: Habilidades e agentes
description: Defina habilidades e agentes de IA para o seu aplicativo.
icon: robot
---
<Warning>
As habilidades e os agentes estão atualmente em testes alfa. O recurso é funcional, mas ainda está evoluindo.
</Warning>
Os aplicativos podem definir capacidades de IA que residem dentro do espaço de trabalho — instruções de habilidades reutilizáveis e agentes com prompts de sistema personalizados.
<AccordionGroup>
<Accordion title="defineSkill" description="Defina habilidades de agentes de IA">
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:
```ts src/skills/example-skill.ts
import { defineSkill } from 'twenty-sdk/define';
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.
</Accordion>
<Accordion title="defineAgent" description="Defina agentes de IA com prompts personalizados">
Os 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:
```ts src/agents/example-agent.ts
import { defineAgent } from 'twenty-sdk/define';
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.
* `responseFormat` (opcional) controla o formato da saída do agente. O padrão é `{ type: 'text' }` para texto em formato livre. Use `{ type: 'json', schema }` para forçar a saída em JSON estruturado.
Por padrão, um agente retorna texto em formato livre. Para obter saída estruturada, defina `responseFormat` como `{ type: 'json' }` e forneça um `schema`:
```ts src/agents/structured-agent.ts
import { defineAgent } from 'twenty-sdk/define';
export default defineAgent({
universalIdentifier: 'c4d5e6f7-a8b9-0123-cdef-456789012345',
name: 'lead-scorer',
label: 'Lead Scorer',
prompt: 'Score the lead and explain your reasoning.',
responseFormat: {
type: 'json',
schema: {
type: 'object',
properties: {
score: { type: 'number', description: 'Lead score from 0 to 100' },
summary: { type: 'string', description: 'Short reasoning for the score' },
},
required: ['score', 'summary'],
additionalProperties: false,
},
},
});
```
Observações sobre o esquema:
* O esquema é um objeto plano: o `type` de cada propriedade deve ser um primitivo (`string`, `number` ou `boolean`). Objetos aninhados e arrays não são suportados.
* `description` (opcional) em cada propriedade orienta o modelo sobre o que colocar ali.
* `required` (opcional) lista as propriedades que o modelo deve sempre retornar.
* `additionalProperties: false` (opcional) proíbe qualquer propriedade não declarada em `properties`.
</Accordion>
<Accordion title="runAgent" description="Executar um agente a partir de uma função de lógica">
`runAgent()` permite que uma função de lógica execute um dos agentes do seu app (com suas habilidades e ferramentas). Identifique o agente pelo `universalIdentifier` que você passou para `defineAgent()`. Passe uma string `prompt` ou um histórico de conversas como `messages` — não ambos:
```ts src/logic-functions/run-enricher.ts
import { runAgent } from 'twenty-sdk/logic-function';
const { result, error, success } = await runAgent({
agentUniversalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
prompt: 'Enrich House Ad <recordId>: fill empty fields from its listing URL.',
});
```
Para bots de múltiplas interações (Slack, Discord, Teams, …), passe o histórico do tópico como `messages` em vez de um único `prompt`:
```ts src/logic-functions/reply-in-thread.ts
import { runAgent } from 'twenty-sdk/logic-function';
const { result, error, success } = await runAgent({
agentUniversalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
messages: [
{ role: 'user', content: 'Who owns Acme?' },
{ role: 'assistant', content: 'Sarah owns the Acme account.' },
{ role: 'user', content: 'What was the last touchpoint?' },
],
});
```
Pontos-chave:
* Forneça **exatamente um** de `prompt` (string) ou `messages` (de 1 a 100 entradas
de `{ role: 'user' | 'assistant', content: string }`).
* O agente é executado sincronamente e pode ler/atualizar registros por conta própria por meio de suas próprias ferramentas — `runAgent()` é resolvido quando a execução é concluída.
* Um app só pode executar os seus próprios agentes.
* O [papel padrão](/l/pt/developers/extend/apps/config/roles) do app deve conceder o sinalizador de permissão `AI` — adicione `SystemPermissionFlag.AI` ao seu `permissionFlagUniversalIdentifiers` (ou defina `canAccessAllTools: true`).
Sem isso, `runAgent()` falha com um erro de permissão.
* Defina um valor generoso de `timeoutSeconds` na função de lógica — execuções de agentes podem levar vários segundos.
* `success` é `true` e `result` é diferente de nulo quando a execução é concluída; em caso de falha `success` é `false`, `result` é nulo e `error` contém o motivo (por exemplo, quando o espaço de trabalho fica sem créditos de IA no meio da execução).
```ts src/roles/default-role.ts
import { defineApplicationRole, SystemPermissionFlag } from 'twenty-sdk/define';
export default defineApplicationRole({
universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
label: 'Default function role',
// runAgent() requires the AI permission flag on the app's default role.
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.AI],
});
```
<Warning>
**Evite loops:** se você chamar `runAgent()` a partir de um gatilho de evento de banco de dados `*.updated` e o agente atualizar o mesmo registro, delimite o gatilho com `updatedFields` para um campo que o agente nunca escreve (por exemplo, a URL de origem) ou verifique se algum campo de destino ainda está vazio antes de chamar `runAgent()`.
</Warning>
### Executando em nome de um membro do workspace
Passe `runAsWorkspaceMemberId` quando a execução for acionada por uma pessoa — por exemplo, um chatbot respondendo a uma mensagem — para que o agente atue como esse membro em vez de como o app:
```ts src/logic-functions/answer-question.ts
const { result } = await runAgent({
agentUniversalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
prompt: 'How many open opportunities do we have?',
runAsWorkspaceMemberId: '20202020-0687-4c41-b707-ed1bfca972a7',
});
```
A execução então atua com a **própria função do membro**: ela pode fazer tudo o que esse
membro pode fazer e nada além disso, os registros que cria são atribuídos a ele, e
as permissões em nível de linha dele se aplicam. A função do agente não participa — ela
é o padrão do aplicativo para execuções sem ninguém por trás delas. Omita o campo para
execuções autônomas (jobs agendados, gatilhos de eventos de banco de dados): essas mantêm a
própria função do agente. O aplicativo atuante permanece vinculado ao contexto da execução
para rastreabilidade, sem restringir as permissões do membro.
Seu app é responsável por mapear a pessoa que acionou a execução para um membro do workspace. Nomear um requer um token de acesso do app, e o que um token pode
nomear depende de o token estar associado a um usuário ou não:
* Um token **sem nenhum usuário associado** pode nomear qualquer membro. Uma função lógica é executada
com um, assim como tokens criados por meio de `client_credentials` ou a partir de uma API
key.
* Um token emitido **em nome de um usuário**, como um front component recebe, só pode
nomear o próprio membro desse usuário.
Qualquer outra coisa — uma sessão simples de usuário, uma API key sem um app token — não pode
nomear nenhum membro.
<Warning>
`runAgent()` gera uma exceção quando o membro do workspace não pode ser resolvido — um
membro desconhecido ou removido, ou um sem função. Ele nunca recorre à
própria função do agente, pois isso concederia mais acesso do que o requisitante pediu.
</Warning>
</Accordion>
</AccordionGroup>

View File

@@ -0,0 +1,131 @@
---
title: CLI
description: comandos `yarn twenty` para executar funções, transmitir logs, gerenciar instalações de aplicativos e alternar remotos.
icon: terminal
---
A CLI `yarn twenty` é a sua interface para tudo relacionado a aplicativos. Lista completa de comandos:
| Comando | O que faz | Documentado em |
| ----------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `dev` | Monitora arquivos-fonte e sincroniza alterações em tempo real | [Início rápido](/l/pt/developers/extend/apps/getting-started/quick-start) |
| `plan` | Visualize as alterações de metadados sem aplicá-las | [Sincronização e recuperação](/l/pt/developers/extend/apps/operations/sync-and-recovery#previewing-changes-plan) |
| `apply` | Aplicar alterações de metadados após exibir o plano | [Sincronização e recuperação](/l/pt/developers/extend/apps/operations/sync-and-recovery) |
| `dev:build` | Compile o aplicativo e gere o cliente da API (`--tarball` para empacotar um `.tgz`) | [Publicação](/l/pt/developers/extend/apps/operations/publishing) |
| `dev:typecheck` | Executar a verificação de tipos do TypeScript | [Testes](/l/pt/developers/extend/apps/operations/testing) |
| `dev:add` | Criar a estrutura inicial de uma nova entidade | [Scaffolding](/l/pt/developers/extend/apps/getting-started/scaffolding) |
| `dev:generate-client` | Regenerar o cliente da API tipado | esta página |
| `dev:function:exec` / `dev:function:logs` | Executar funções e transmitir seus logs | esta página |
| `dev:translations-extract` | Extrair strings traduzíveis para catálogos em `locales/` | [Traduções](/l/pt/developers/extend/apps/operations/translations) |
| `dev:catalog-sync` | Acionar a sincronização do catálogo do marketplace | [Publicação](/l/pt/developers/extend/apps/operations/publishing#how-marketplace-discovery-works) |
| `app:publish` / `app:install` / `app:uninstall` | Ciclo de vida de versões | [Publicação](/l/pt/developers/extend/apps/operations/publishing) e esta página |
| `docker:*` | Gerenciar o contêiner do servidor local do Twenty | [Servidor local](/l/pt/developers/extend/apps/getting-started/local-server) |
| `remote:*` | Gerenciar conexões de servidor | esta página |
Todo comando aceita `-r, --remote \<name>` para direcionar a um remoto específico em vez do padrão.
## Executando funções (`yarn twenty dev:function:exec`)
Execute manualmente uma função de lógica sem acioná-la via HTTP, cron ou evento de banco de dados:
```bash filename="Terminal"
# Execute by function name
yarn twenty dev:function:exec -n create-new-post-card
# Execute by universalIdentifier
yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
# Pass a JSON payload
yarn twenty dev:function:exec -n create-new-post-card -p '{"name": "Hello"}'
# Execute the install and uninstall hooks
yarn twenty dev:function:exec --postInstall
yarn twenty dev:function:exec --preInstall
yarn twenty dev:function:exec --uninstall
```
## Visualizando logs de funções (`yarn twenty dev:function:logs`)
Transmita os logs de execução das funções de lógica do seu aplicativo:
```bash filename="Terminal"
# Stream all function logs
yarn twenty dev:function:logs
# Filter by function name
yarn twenty dev:function:logs -n create-new-post-card
# Filter by universalIdentifier
yarn twenty dev:function:logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
```
<Note>
Isso é diferente de `yarn twenty docker:logs`, que mostra os logs do contêiner Docker. `yarn twenty dev:function:logs` mostra os logs de execução de funções do seu aplicativo a partir do servidor Twenty.
</Note>
## Gerando o cliente tipado (`yarn twenty dev:generate-client`)
Regenerar o cliente de API tipado (`twenty-client-sdk`) a partir do esquema do remoto ativo, sem compilar nem sincronizar um aplicativo. Use-o para obter um cliente tipado em qualquer projeto — como um serviço de backend em um repositório separado — que se comunica com a sua instância do Twenty:
```bash filename="Terminal"
# In your project (no Twenty app definition required)
yarn add twenty-sdk twenty-client-sdk
# Connect to the Twenty instance to generate the client from
yarn twenty remote:add
# Generate the typed client into node_modules/twenty-client-sdk
yarn twenty dev:generate-client
```
Em seguida, importe o cliente no seu código:
```typescript
import { CoreApiClient } from 'twenty-client-sdk/core';
```
Execute novamente o comando sempre que o seu modelo de dados for alterado para atualizar os tipos gerados.
<Note>
O cliente é gerado dentro de `node_modules`, portanto não é incluído no commit junto com o seu código. Execute `yarn twenty dev:generate-client` após cada instalação (por exemplo, em um script `postinstall` ou no CI).
</Note>
## Desinstalando um aplicativo (`yarn twenty app:uninstall`)
Remova seu aplicativo do espaço de trabalho ativo:
```bash filename="Terminal"
yarn twenty app:uninstall
# Skip the confirmation prompt
yarn twenty app: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.
```bash filename="Terminal"
# 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 --url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote
# List all configured remotes
yarn twenty remote:list
# Set the active remote
yarn twenty remote:use <name>
# Check that the active remote's authentication is still valid
yarn twenty remote:status
# Remove a remote
yarn twenty remote:remove <name>
```
Suas credenciais são armazenadas em `~/.twenty/config.json`.

View File

@@ -0,0 +1,32 @@
---
title: Visão Geral
description: Crie, teste e envie seu app — comandos de CLI, testes de integração, CI e publicação em um servidor ou no npm.
icon: rocket
---
A **camada de operações** é tudo o que você faz *para* o seu app em vez de *com* ele: invocar comandos de CLI, executar testes de integração contra um servidor Twenty real, configurar CI e enviar versões — seja como um tarball implantado em um único servidor ou como um pacote npm listado no marketplace.
```text
develop ─▶ test ─▶ build ─▶ deploy / publish
─────── ──── ───── ─────────────────
yarn yarn yarn yarn twenty app:publish --private (tarball → one server)
twenty test twenty
dev dev:build yarn twenty app:publish (npm → marketplace)
```
## Nesta secção
<CardGroup cols={2}>
<Card title="CLI" icon="terminal" href="/l/pt/developers/extend/apps/operations/cli">
Referência do `yarn twenty` — exec, logs, uninstall, remotes.
</Card>
<Card title="Sincronização e recuperação" icon="compass" href="/l/pt/developers/extend/apps/operations/sync-and-recovery">
Qual comando usar e quando, leitura do diff de sincronização e etapas de recuperação.
</Card>
<Card title="Testes" icon="flask" href="/l/pt/developers/extend/apps/operations/testing">
Configuração do Vitest, testes de integração, verificação de tipos, fluxo de trabalho de CI.
</Card>
<Card title="Publicação" icon="upload" href="/l/pt/developers/extend/apps/operations/publishing">
Criar, implantar um tarball, publicar no npm, instalar.
</Card>
</CardGroup>

View File

@@ -0,0 +1,293 @@
---
title: Publicação
icon: upload
description: Distribua seu aplicativo Twenty no Marketplace ou implante-o internamente.
---
## Visão Geral
Depois que seu aplicativo estiver [compilado e testado localmente](/l/pt/developers/extend/apps/getting-started/concepts), você tem dois caminhos para distribuí-lo:
* **Implantar um tarball** — envie seu aplicativo diretamente para um servidor Twenty específico para uso interno ou privado.
* **Publicar no npm** — liste seu aplicativo no Marketplace da Twenty para que qualquer espaço de trabalho possa descobrir e instalar.
Ambos os caminhos começam na mesma etapa de **build**.
## Compilando seu app
Execute o comando build para compilar seu app e gerar um `manifest.json` pronto para distribuição:
```bash filename="Terminal"
yarn twenty dev:build
```
Isso compila seu código-fonte em TypeScript, transpila funções de lógica e componentes de front-end e grava tudo em `.twenty/output/`. Adicione `--tarball` para também gerar um pacote `.tgz` para distribuição manual ou para o comando de publish.
## Implantando em um servidor (tarball)
Para aplicativos que você não quer disponibilizar publicamente — ferramentas proprietárias, integrações apenas para empresas ou builds experimentais — você pode implantar um tarball diretamente em um servidor Twenty.
### Pré-requisitos
Antes de implantar, você precisa de um remote configurado apontando para o servidor de destino. Os remotes armazenam a URL do servidor e as credenciais de autenticação localmente em `~/.twenty/config.json`.
Adicionar um remote:
```bash filename="Terminal"
yarn twenty remote:add --url https://your-twenty-server.com --as production
```
### Implantando
Compile e envie seu aplicativo para o servidor em uma única etapa:
```bash filename="Terminal"
yarn twenty app:publish --private
# To deploy to a specific remote:
# yarn twenty app:publish --private --remote production
```
### Compartilhando um aplicativo implantado
Aplicativos em tarball não são listados no marketplace público, então outros espaços de trabalho no mesmo servidor não os descobrirão ao navegar. Para compartilhar um aplicativo implantado:
1. Vá para **Configurações > Aplicações > Registros** e abra seu aplicativo
2. Na guia **Distribuição**, clique em **Copiar link de compartilhamento**
3. Compartilhe esse link com usuários de outros espaços de trabalho — ele os leva diretamente para a página de instalação do aplicativo
O link de compartilhamento usa a URL base do servidor (sem qualquer subdomínio de espaço de trabalho), para funcionar em qualquer espaço de trabalho no servidor.
### Gerenciamento de versões
Ao atualizar um aplicativo empacotado como tarball já implantado, o servidor exige que o `version` no `package.json` seja **estritamente maior** (de acordo com a ordenação do [semver](https://semver.org)) do que a versão atualmente implantada. Reimplantar a mesma versão, ou enviar uma inferior, é rejeitado antes que o tarball seja armazenado — você verá um erro `VERSION_ALREADY_EXISTS` na CLI.
Para lançar uma atualização:
1. Atualize o campo `version` no seu `package.json` (por exemplo, `1.2.3` → `1.2.4`, `1.3.0` ou `2.0.0`)
2. Execute `yarn twenty app:publish --private` (ou `yarn twenty app:publish --private --remote production`)
3. Espaços de trabalho que têm o app instalado e ativaram a atualização automática para o app (na aba Configurações do app) são atualizados automaticamente em segundo plano; os demais verão a atualização disponível em suas configurações
<Note>
Tags de pré-lançamento funcionam como esperado: incrementar `1.0.0-rc.1` → `1.0.0-rc.2` é permitido, e uma versão final como `1.0.0` é corretamente reconhecida como superior a `1.0.0-rc.5`. A versão em `package.json` deve ser, ela própria, uma string semver válida.
</Note>
{/* TODO: add screenshot of the Upgrade button */}
### Compatibilidade da versão do servidor
Se o seu aplicativo usar um recurso introduzido em uma versão específica do servidor Twenty (por exemplo, provedores OAuth adicionados na v2.3.0), você deve declarar a versão mínima do servidor que seu aplicativo requer usando o campo `engines.twenty` em `package.json`:
```json filename="package.json"
{
"name": "twenty-my-app",
"version": "1.0.0",
"engines": {
"node": "^24.5.0",
"twenty": ">=2.3.0"
}
}
```
O valor é um [intervalo semver](https://github.com/npm/node-semver#ranges) padrão. Padrões comuns:
| Intervalo | Significado |
| ---------------------------------- | ---------------------------------------------------------- |
| `>=2.3.0` | Qualquer servidor a partir de 2.3.0 |
| `>=2.3.0 \<3.0.0` | 2.3.0 ou posterior, mas abaixo da próxima versão principal |
| `^2.3.0` | O mesmo que `>=2.3.0 \<3.0.0` |
**O que acontece no momento da implantação e da instalação:**
* Se `engines.twenty` estiver definido e a versão do servidor de destino não satisfizer o intervalo, a implantação (upload do tarball) ou a instalação será rejeitada com o erro `SERVER_VERSION_INCOMPATIBLE` e uma mensagem indicando tanto o intervalo exigido quanto a versão real do servidor.
* Se `engines.twenty` **não estiver definido**, o aplicativo é aceito em qualquer versão do servidor (retrocompatível com os aplicativos existentes).
* Se o servidor não tiver `APP_VERSION` configurado, a verificação será ignorada.
<Note>
O servidor realiza a verificação definitiva — ele valida `engines.twenty` tanto no upload do tarball quanto na instalação no espaço de trabalho. Se você implantar um tarball fora de banda ou instalar a partir do marketplace, o servidor ainda impõe a compatibilidade.
</Note>
## CI/CD automatizado (fluxos de trabalho pré-configurados)
Os apps gerados com `create-twenty-app` já vêm com três fluxos de trabalho do GitHub Actions prontos, em `.github/workflows/`. A CI é executada sem nenhuma configuração, a CD requer um único segredo e a publicação no npm exige uma configuração única de trusted-publisher do npm.
### CI — `ci.yml`
Executa testes de integração a cada push para `main` e a cada pull request.
**O que faz:**
1. Faz checkout do código-fonte do seu app.
2. Inicia uma instância de teste do Twenty isolada usando a ação composta `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main` (o equivalente em CI de `yarn twenty docker:start --test`).
3. Habilita o Corepack, configura o Node.js a partir do seu `.nvmrc` e instala as dependências com `yarn install --immutable`.
4. Executa `yarn test`, passando `TWENTY_API_URL` e `TWENTY_API_KEY` da instância iniciada para que seus testes possam se comunicar com um servidor real.
**Opções de configuração:**
* `TWENTY_VERSION` (env, padrão `latest`) — fixe a versão do servidor Twenty usada no CI editando isto em `ci.yml`.
* A concorrência é agrupada por `github.ref` e cancela execuções em andamento quando há novos pushes.
Nenhum segredo é necessário — a instância de teste é efêmera e existe apenas durante a execução do job.
### CD — `cd.yml`
Faz o deploy do seu app para um servidor Twenty configurado a cada push para `main` e, opcionalmente, a partir de um pull request quando o rótulo `deploy` é aplicado.
**O que faz:**
1. Faz checkout do head do PR (para PRs rotulados) ou do commit enviado.
2. Executa `twentyhq/twenty/.github/actions/deploy-twenty-app@main` — o equivalente em CI de `yarn twenty app:publish --private`.
3. Executa `twentyhq/twenty/.github/actions/install-twenty-app@main` para que a versão recém-implantada seja instalada no espaço de trabalho de destino.
**Configuração obrigatória:**
| Configuração | Onde | Finalidade |
| ----------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `TWENTY_DEPLOY_URL` | `env` em `cd.yml` (padrão `http://localhost:3000`) | O servidor Twenty para o qual fazer o deploy. Altere isto para a URL real do seu servidor antes do primeiro uso. |
| `TWENTY_DEPLOY_API_KEY` | Repositório do GitHub **Settings → Secrets and variables → Actions** | Chave de API com permissão de deploy no servidor de destino. |
<Note>
O `TWENTY_DEPLOY_URL` padrão de `http://localhost:3000` é um placeholder — ele não alcançará nada a partir de um runner hospedado pelo GitHub. Atualize-o para a URL pública do seu servidor (ou use um runner self-hosted com acesso à rede) antes de habilitar o CD.
</Note>
**Acionando um deploy de pré-visualização a partir de um PR:**
Adicione o rótulo `deploy` a um pull request. A condição `if:` em `cd.yml` executará o job para esse PR usando o commit HEAD do PR, permitindo que você valide uma alteração no servidor de destino antes de fazer o merge.
### Publicar — `publish.yml`
Publica seu app no npm com procedência quando você envia uma tag de versão (por exemplo, `v1.0.0`), ou quando executa o fluxo de trabalho manualmente na aba Actions.
**O que faz:**
1. Faz o checkout do seu app, configura o Node.js e atualiza o npm (a publicação confiável requer o npm 11.5.1 ou posterior).
2. Executa `yarn twenty app:publish`, que compila o app e publica `.twenty/output` no npm. Em CI, adiciona automaticamente `--provenance` e `--access public`, portanto não são necessárias flags no fluxo de trabalho.
**Configuração única:**
Em npmjs.com, abra o seu pacote > **Settings → Trusted Publisher** e registre este repositório com o fluxo de trabalho `publish.yml` (consulte a [documentação de publicação confiável do npm](https://docs.npmjs.com/trusted-publishers)). Publicar com proveniência certifica qual repositório do GitHub compilou o pacote, o que também é como você reivindica a propriedade do seu app em um marketplace da Twenty.
<Note>
npm só aceita proveniência de repositórios de código-fonte **públicos**. Se você publicar a partir de um repositório privado, o npm rejeita o pacote de proveniência OIDC com um `E422 ... Erro: `Unsupported GitHub Actions source repository visibility: "private"`. Para publicar a partir de um repositório privado, desative a proveniência definindo `TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'`no`env`da etapa de publicação (uma dica comentada está incluída no`publish.yml\` gerado pelo scaffold):
```yaml filename=".github/workflows/publish.yml"
- name: Publish to npm
env:
TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'
run: yarn twenty app:publish
```
</Note>
### Fixando as ações reutilizáveis
Os fluxos de trabalho `ci.yml` e `cd.yml` fazem referência a ações reutilizáveis em `@main`, portanto as atualizações de ações no repositório `twentyhq/twenty` são aplicadas automaticamente. Se você quiser builds determinísticos, substitua `@main` por um SHA de commit ou uma tag de release em cada linha `uses:`.
## Publicação no npm
Publicar no npm torna seu aplicativo descobrível no Marketplace da Twenty. Qualquer espaço de trabalho da Twenty pode navegar, instalar e atualizar aplicativos do Marketplace diretamente pela UI.
### Requisitos
* Uma conta no [npm](https://www.npmjs.com)
* A palavra-chave `twenty-app` no array `keywords` do seu `package.json` (adicione-a manualmente — não é incluída por padrão no template `create-twenty-app`)
```json filename="package.json"
{
"name": "twenty-app-postcard-sender",
"version": "1.0.0",
"keywords": ["twenty-app"]
}
```
### Metadados do Marketplace
A configuração `defineApplication()` oferece suporte a campos opcionais que controlam como seu app aparece no marketplace. Use `logo` e `galleryImages` para referenciar imagens da pasta `public/`:
```ts src/application-config.ts
export default defineApplication({
universalIdentifier: '...',
displayName: 'My App',
description: 'A great app',
logo: 'public/logo.png',
galleryImages: [
'public/screenshot-1.png',
'public/screenshot-2.png',
],
});
```
Veja o [acordeão de defineApplication](/l/pt/developers/extend/apps/config/application#marketplace-metadata) na página Building Apps para a lista completa de campos do marketplace (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl`, etc.).
#### Dimensões recomendadas para imagens da galeria
O marketplace renderiza `galleryImages` em um contêiner fixo de `8:5` (por exemplo, `1600×1000 px`).
<Note>
Imagens da galeria de qualquer proporção são exibidas por completo e nunca são cortadas, mas qualquer coisa significativamente mais alta ou mais estreita que `8:5` exibirá faixas vazias nas laterais.
</Note>
#### Limite de tamanho de imagem
O arquivo `logo` e cada arquivo em `galleryImages` não devem exceder **10 MB**. Arquivos maiores são ignorados quando o marketplace volta a hospedar os seus recursos publicados, portanto eles não serão exibidos.
### Publicar
```bash filename="Terminal"
yarn twenty app:publish
```
Para publicar sob uma dist-tag específica (por exemplo, `beta` ou `next`):
```bash filename="Terminal"
yarn twenty app:publish --tag beta
```
### Como funciona a descoberta no marketplace
O servidor Twenty sincroniza seu catálogo do marketplace a partir do registro do npm **a cada hora**.
Você pode acionar a sincronização imediatamente em vez de esperar:
```bash filename="Terminal"
yarn twenty dev:catalog-sync
# To target a specific remote:
# yarn twenty dev:catalog-sync --remote production
```
Os metadados exibidos no marketplace vêm da sua configuração de `defineApplication()` — consulte [Metadados do marketplace](#marketplace-metadata) acima.
<Note>
Se o seu aplicativo não definir um `aboutDescription` em `defineApplication()`, o marketplace usará automaticamente o `README.md` do seu pacote no npm como conteúdo da página Sobre. Isso significa que você pode manter um único README tanto para o npm quanto para o marketplace da Twenty. Se quiser uma descrição diferente no marketplace, defina explicitamente `aboutDescription`.
</Note>
### Publicação via CI
O fluxo de trabalho `publish.yml` estruturado conforme descrito acima publica automaticamente no npm em tags de versão, com proveniência. Como `yarn twenty app:publish` adiciona `--provenance` e `--access public` para você quando é executado em CI, o fluxo de trabalho não precisa de flags do npm — apenas da configuração única de trusted publisher.
Para outros sistemas de CI (GitLab CI, CircleCI, etc.), execute `yarn install` e depois `yarn twenty app:publish`. A proveniência é emitida quando o ambiente pode gerar um token OIDC e é ignorada automaticamente caso contrário.
<Note>
A **proveniência do npm** adiciona um selo de confiança à sua listagem no npm, permitindo que os usuários verifiquem que o pacote foi construído a partir de um commit específico em um pipeline de CI público. É também o que permite que você reivindique a propriedade do seu app em um marketplace da Twenty. Consulte a [documentação de proveniência do npm](https://docs.npmjs.com/generating-provenance-statements) para mais detalhes.
</Note>
## Instalando aplicativos
Depois que um app é publicado (npm) ou implantado (tarball), os espaços de trabalho podem instalá-lo pela interface do usuário.
Vá para a página **Configurações > Aplicações** no Twenty, onde é possível navegar e instalar tanto apps do marketplace quanto apps implantados por tarball.
{/* TODO: add screenshot of the UI when the app is registered */}
Você também pode instalar apps pela linha de comando:
```bash filename="Terminal"
yarn twenty app:install
```
<Note>
O servidor impõe o versionamento semver na instalação, espelhando as regras da implantação:
* Instalar a mesma versão que já está instalada no seu espaço de trabalho é rejeitado com um erro `APP_ALREADY_INSTALLED`.
* Instalar uma versão inferior à atualmente instalada é rejeitado com um erro `CANNOT_DOWNGRADE_APPLICATION`.
Para instalar uma versão mais recente, implante ou publique-a primeiro e, em seguida, execute novamente `yarn twenty app:install`.
</Note>

View File

@@ -0,0 +1,126 @@
---
title: Sincronização e recuperação
description: Qual comando usar em cada caso, como ler a saída da sincronização e uma escada de recuperação para quando os metadados locais se desalinham — antes de chegar a um reset completo.
icon: compass
---
O desenvolvimento local de apps gira em torno da **sincronização**: a CLI reconstrói seu manifesto e o servidor aplica apenas a diferença entre ele e os metadados que já estão no seu workspace. Esta página cobre qual comando usar, como ler o que uma sincronização mudou e o que fazer — em ordem — quando o estado local parece inconsistente.
## Qual comando usar e quando
<Note>
Para a iteração local do dia a dia, quase sempre você vai querer `yarn twenty dev`. Fazer deploy e publicar servem para entregar releases, **não** para o ciclo local.
</Note>
| Você quer… | Comando | Notas |
| ------------------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| Iterar localmente com sincronização em tempo real | `yarn twenty dev` | Monitora seus arquivos e sincroniza a cada alteração. |
| Sincronizar uma vez e sair (CI, scripts, hooks) | `yarn twenty apply` | Um build + sincronização, depois encerra. Adicione `--force` para pular a confirmação de mudança destrutiva. |
| Prever mudanças **sem aplicá-las** | `yarn twenty plan` | Calcula e imprime o diff; não grava nada. |
| Remover o app do workspace | `yarn twenty app:uninstall` | Adicione `--yes` para pular o prompt. |
| Enviar um tarball para um servidor | `yarn twenty app:publish --private` | Requer uma versão **estritamente maior** em `package.json` — veja [Publicação](/l/pt/developers/extend/apps/operations/publishing). |
| Publicar no marketplace (npm) | `yarn twenty app:publish` | — |
| Instalar / atualizar uma versão implantada | `yarn twenty app:install` | Instala a versão atualmente implantada. |
| Limpar o servidor local e começar do zero | `yarn twenty docker:reset` | Exclui **todos** os dados locais — último recurso. |
<Note>
`yarn twenty dev --once` e `yarn twenty dev --once --dry-run` ainda funcionam como aliases obsoletos para `yarn twenty apply` e `yarn twenty plan`.
</Note>
### A sincronização local não precisa de incremento de versão
A regra de `version` estritamente crescente (`VERSION_ALREADY_EXISTS` no deploy, `APP_ALREADY_INSTALLED` / `CANNOT_DOWNGRADE_APPLICATION` na instalação) se aplica a **`app:publish` / `app:install`** — o caminho de release. `yarn twenty dev` sincroniza seu manifesto no lugar e nunca exige mudança de versão, então você não precisa mexer em `package.json` para iterar. Se você se pegar aumentando a versão para testar uma mudança local, está usando o caminho de release quando o que quer é o ciclo de desenvolvimento.
## Lendo a saída da sincronização
Cada sincronização imprime as alterações de metadados que aplicou (ou aplicaria, com `plan`), no estilo do Terraform — um bloco por entidade com seus atributos, depois uma linha de resumo:
```text filename="Terminal"
# objectMetadata "rocket" will be created
+ icon = "IconRocket"
+ labelSingular = "Rocket"
+ ...
# fieldMetadata "launchedAt" will be updated
~ isNullable = false -> true
Plan: 2 to add, 1 to change, 1 to destroy.
✓ Synced My App (4 files)
```
Este é seu primeiro diagnóstico: ele mostra exatamente quais objetos, campos e layouts mudaram, para que você possa confirmar que uma sincronização fez o que esperava antes de conferir na interface.
Mudanças destrutivas (`to destroy`) são listadas com o que elas removem (por exemplo, `objectMetadata "auditNote" — drops the table and all its rows`) e exigem confirmação interativa, ou `--force` em scripts.
Quando uma sincronização falha em uma única entidade, o erro nomeia a entidade com problema e seu `universalIdentifier`, por exemplo:
```text
Migration action 'create' for 'fieldMetadata' (universalIdentifier: 2020...4337) failed
```
Use esse identificador para encontrar a entidade no seu manifesto (e, se necessário, no workspace), em vez de adivinhar qual está em conflito.
## Visualizando mudanças (plan)
`yarn twenty plan` compila seu manifesto, pede ao servidor o plano de migração e o imprime — **sem aplicar nada**. É a forma segura de responder "o que esta sincronização mudaria?" antes de se comprometer com ela.
```bash filename="Terminal"
yarn twenty plan
```
```text filename="Terminal"
Building manifest...
Computing metadata plan (read-only, nothing will be applied)...
# fieldMetadata "crewCapacity" will be created
+ ...
Plan: 1 to add, 1 to change, 0 to destroy.
✓ Plan complete for My App — no changes were applied
```
Um plano:
* **Não grava nada** — nenhuma migração de metadados, nenhuma atualização de registro de aplicativo, nenhuma mudança de função/aba padrão e nenhuma geração de cliente de API.
* Retorna o **mesmo diff** que uma sincronização real aplicaria, para que você possa revisar previamente as entidades criadas/atualizadas/excluídas.
* É útil antes de uma mudança arriscada, ao revisar uma mudança gerada por IA ou em um script que deve falhar se uma mudança inesperada estiver prestes a ser aplicada.
<Note>
Um plano só antevê mudanças de **metadados**, e exige que o app tenha sido sincronizado ao menos uma vez (para que o workspace o conheça). Se você rodar isso em um app que nunca foi sincronizado, o servidor informa que o app não está instalado — rode `yarn twenty dev` uma vez antes.
</Note>
## Escada de recuperação
Quando os metadados locais parecerem errados, aumente o nível nesta ordem e pare assim que estiver desbloqueado. Cada etapa é mais disruptiva que a anterior.
1. **Ressincronizar.** Rode `yarn twenty apply` novamente. Sincronizações são idempotentes — rodar novamente um manifesto limpo é seguro e frequentemente resolve um problema transitório.
2. **Prever o plano.** Rode `yarn twenty plan` para ver exatamente o que a próxima sincronização pretende mudar, sem aplicá-la.
3. **Leia o erro nomeado.** Se uma sincronização falhar, anote o tipo de metadado e o `universalIdentifier` na mensagem (veja acima) e localize essa entidade no seu manifesto. Um conflito geralmente aponta para um identificador duplicado ou reutilizado.
4. **Desinstalar e reinstalar.** `yarn twenty app:uninstall`, depois sincronize novamente (`yarn twenty dev`). Isso reconstrói os metadados do app a partir do zero, mantendo o restante do seu workspace intacto.
5. **Reset completo (último recurso).** `yarn twenty docker:reset`, depois faça o seeding e a sincronização novamente.
<Warning>
`yarn twenty docker:reset` exclui **todos** os dados da sua instância local — todos os workspaces, registros e apps. Use isso somente depois que as etapas anteriores tiverem falhado.
</Warning>
<Note>
Encontrou um erro de metadados? Por favor, [abra uma issue](https://github.com/twentyhq/twenty/issues/new/choose) e inclua a mensagem de migração que falhou (com seu tipo de metadado e `universalIdentifier`), a saída de `Metadata changes` da sincronização e os comandos que você rodou.
</Note>
## Evite sincronizações concorrentes em um único workspace
Sincronizar aplica migrações de metadados. Executar várias operações de sincronização, deploy ou instalação contra o **mesmo workspace ao mesmo tempo** — por exemplo, múltiplos terminais ou agentes de IA iterando em paralelo — pode intercalar essas migrações e deixar os metadados em um estado parcialmente aplicado.
O servidor serializa sincronizações por workspace para evitar isso, mas você ainda deve direcionar operações sensíveis de metadados por um **único** processo em vez de dispará-las concorrentemente. Se você orquestra o desenvolvimento com múltiplos agentes, encaminhe as chamadas de sincronização/deploy/instalação deles por uma única fila, para que apenas uma rode por vez.
## Diferenciando tipos de falha
Quando algo dá errado, o diff de metadados e os erros nomeados permitem localizar a falha:
* **Erro de build do manifesto** — a CLI falha antes de sincronizar (`MANIFEST_BUILD_FAILED`, `TYPECHECK_FAILED`); corrija o código-fonte do seu app.
* **Erro de sincronização / migração** — o build é bem-sucedido, mas aplicar o diff falha, nomeando a entidade e o `universalIdentifier`; corrija os metadados em conflito.
* **Erro no tamanho das dependências** — a sincronização ou instalação falha porque as `dependencies` de produção do app são grandes demais para serem instaladas como uma camada de runtime (`LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED`); mova para `devDependencies` os pacotes que suas funções de lógica não importam em tempo de execução (bibliotecas de UI, ferramentas de desenvolvimento).
* **Erro de tempo de execução no código do app** — a sincronização é concluída com êxito, mas suas funções de lógica ou componentes se comportam de forma incorreta em tempo de execução; verifique os [logs de função](/l/pt/developers/extend/apps/operations/cli).
* **Estado da instância local** — nenhuma das opções acima e o espaço de trabalho ainda parece incorreto; prossiga descendo na escada de recuperação.

View File

@@ -0,0 +1,264 @@
---
title: Testes
description: Configuração do Vitest, testes de integração contra um servidor Twenty real, verificação de tipos e CI com GitHub Actions.
icon: flask
---
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](https://vitest.dev/) 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.
## 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](https://esbuild.github.io/), que incorpora suas dependências na saída — nenhum `node_modules` é necessário em tempo de execução.
### Instalando um pacote
```bash filename="Terminal"
yarn add axios
```
Em seguida, importe-o no seu código:
```ts src/logic-functions/fetch-data.ts
import { defineLogicFunction } from 'twenty-sdk/define';
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:
```tsx src/front-components/chart.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
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 usa o esbuild para produzir um único arquivo independente por função lógica e por componente de front-end. Pacotes importados são incorporados ao bundle, exceto aqueles listados abaixo.
**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 internos do nó **não** estão disponíveis — apenas pacotes npm que funcionam em um ambiente de navegador. Note que a sandbox implementa um *parcial* DOM, para que um pacote possa ser construído de forma limpa e ainda falhar no tempo de execução; veja [Limitação atual](/l/pt/developers/extend/apps/layout/front-components#current-limitations).
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.
Componentes de front-end também podem compartilhar dependências por meio de um [bundle de dependências compartilhadas](/l/pt/developers/extend/apps/layout/front-components#sharing-dependencies-across-front-components). Os pacotes listados no campo `frontComponentSharedDependencies` do package.json do app são compilados uma vez em um arquivo separado e carregados a partir dele em tempo de execução, em vez de serem incorporados em cada componente. Assim, um componente que importa apenas pacotes compartilhados praticamente não carrega código de dependência próprio.
## Configuração
O aplicativo gerado pelo scaffolder já inclui o Vitest. Se você configurá-lo manualmente, instale as dependências:
```bash filename="Terminal"
yarn add -D vitest vite-tsconfig-paths
```
Crie um `vitest.config.ts` na raiz do seu aplicativo:
```ts vitest.config.ts
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
const TWENTY_API_KEY = process.env.TWENTY_API_KEY ?? '<the pre-seeded local dev key>';
// Make env vars available to globalSetup (test.env only applies to workers)
process.env.TWENTY_API_URL = TWENTY_API_URL;
process.env.TWENTY_API_KEY = TWENTY_API_KEY;
export default defineConfig({
plugins: [
tsconfigPaths({
projects: ['tsconfig.spec.json'],
ignoreConfigErrors: true,
}),
],
test: {
testTimeout: 120_000,
hookTimeout: 120_000,
fileParallelism: false,
include: ['src/**/*.integration-test.ts'],
globalSetup: ['src/__tests__/global-setup.ts'],
env: {
TWENTY_API_URL,
TWENTY_API_KEY,
},
},
});
```
Crie um arquivo de configuração global que verifique se o servidor está acessível, escreva uma configuração de teste para o SDK (`~/.twenty/config.test.json`) e sincronize o app antes da execução dos testes:
```ts src/__tests__/global-setup.ts
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { appDevOnce, appUninstall } from 'twenty-sdk/cli';
const APP_PATH = process.cwd();
const CONFIG_DIR = path.join(os.homedir(), '.twenty');
export async function setup() {
const apiUrl = process.env.TWENTY_API_URL!;
const apiKey = process.env.TWENTY_API_KEY!;
// Verify the server is running
const response = await fetch(`${apiUrl}/healthz`);
if (!response.ok) {
throw new Error(`Twenty server is not reachable at ${apiUrl}.`);
}
// Write the SDK's test config (the CLI reads config.test.json when NODE_ENV=test)
fs.mkdirSync(CONFIG_DIR, { recursive: true });
fs.writeFileSync(
path.join(CONFIG_DIR, 'config.test.json'),
JSON.stringify({
remotes: { local: { apiUrl, apiKey } },
defaultRemote: 'local',
}, null, 2),
);
// Start from a clean slate, then sync the app
await appUninstall({ appPath: APP_PATH }).catch(() => {});
const result = await appDevOnce({ appPath: APP_PATH });
if (!result.success) {
throw new Error(`Dev sync failed: ${result.error?.message}`);
}
}
export async function teardown() {
await appUninstall({ appPath: APP_PATH });
}
```
## 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ção | Descrição |
| -------------- | ---------------------------------------------------------------- |
| `appBuild` | Compilar o aplicativo e, opcionalmente, empacotar um tarball |
| `appDeploy` | Enviar um tarball para o servidor |
| `appDevOnce` | Compila e sincroniza o app uma vez (igual a `yarn twenty apply`) |
| `appInstall` | Instalar o aplicativo no espaço de trabalho ativo |
| `appUninstall` | Desinstalar 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:
```ts 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:
```bash filename="Terminal"
yarn test
```
Ou no modo watch durante o desenvolvimento:
```bash filename="Terminal"
yarn test:watch
```
## Verificação de tipos
Você também pode executar a verificação de tipos no seu aplicativo sem executar os testes:
```bash filename="Terminal"
yarn twenty dev:typecheck
```
Isso executa `tsc --noEmit` no `tsconfig.json` do seu app e informa quaisquer erros de tipo. Os apps criados pelo scaffold também incluem um script `yarn typecheck` que também cobre arquivos de teste (`tsconfig.spec.json`).
## CI com GitHub Actions
O gerador de scaffold cria um workflow pronto para uso em `.github/workflows/ci.yml`. A cada push para `main` e a cada pull request, ele inicia um servidor Twenty efêmero no runner (por meio da action `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test`) e então executa `yarn lint`, `yarn typecheck`, `yarn test:unit` e `yarn test` com `TWENTY_API_URL` / `TWENTY_API_KEY` apontando para esse servidor. Nenhum secret é necessário e você pode fixar a versão do servidor por meio da variável de ambiente `TWENTY_VERSION` no topo do workflow.
Consulte [Publicação → CI/CD automatizado](/l/pt/developers/extend/apps/operations/publishing#automated-cicd-scaffolded-workflows) para um passo a passo completo dos três workflows criados pelo scaffold (`ci.yml`, o pipeline de deploy `cd.yml` e `publish.yml` para publicação no npm).

View File

@@ -0,0 +1,143 @@
---
title: Traduções
description: Publique seu aplicativo em vários idiomas — traduza rótulos do manifesto e strings da interface do usuário por meio de um único catálogo de localidades.
icon: language
---
Os aplicativos Twenty são escritos em **inglês**: as strings no seu código-fonte e no manifesto
são o texto-fonte em inglês, `en` é a localidade de origem a partir da qual você traduz, e
qualquer localidade deixada sem tradução reverte para essa localidade.
Seu aplicativo tem dois tipos de texto traduzível, e ambos passam pelo mesmo
catálogo `locales/`:
* **Rótulos do manifesto** — nomes de objetos e campos, títulos de exibição, itens de menu e
outras strings declaradas nos metadados do seu aplicativo.
* **Front-component strings** — A interface do usuário que processa seus componentes frontais do React.
Você marca as strings traduzíveis, extraa-as em catálogos por localidade,
traduza os catálogos, e a compilação serve a linguagem certa para o usuário atual
— sem asas extras.
## Marcando strings front-component
Importe os auxiliares de tradução de `vinte e sdk/front-component`:
```tsx
import { Trans, t, msg, useTranslate } from 'twenty-sdk/front-component';
const STATUSES = [
{ id: 'draft', label: msg('Draft') },
{ id: 'sent', label: msg('Sent') },
];
const Card = ({ count, name }: { count: number; name: string }) => {
const { t } = useTranslate();
return (
<section>
{/* Static text — reactive to the user's locale */}
<Trans>Loading postcard…</Trans>
{/* Disambiguate identical sources with a context */}
<Trans context="card-title">Untitled</Trans>
{/* Interpolation: pass values explicitly */}
<p>{t('Hi {name}', { name })}</p>
<p>{t('Saved {count} cards', { count })}</p>
{/* Resolve a lazily-declared descriptor */}
<ul>{STATUSES.map((s) => <li key={s.id}>{t(s.label)}</li>)}</ul>
</section>
);
};
```
### Quando usar qual
* **`Trans`** — texto estático em JSX. Use as props `message` e `values` para
interpolação, como em `Trans message="Hi {name}" values={{ name }}`;
a interpolação diretamente em children não pode ser extraída estaticamente.
* **`useTranslate().t`** — strings dinâmicas dentro de um componente. Rerenderiza quando o usuário
muda de linguagem. Prefere isso dentro de renderização.
* **`t(...)`** (importado diretamente) — tradução ávida utilizável **em qualquer lugar**,
incluindo manipuladores de eventos, auxiliares e escopo de módulo — não apenas dentro de renderização.
* **`msg(...)`** — um descritor preguiçoso para frases declaradas como dados (constantes,
config). Resolva-o mais tarde com `t(descriptor)`.
### Contexto
Passe `context` para desambiguar sequências de caracteres de origem idênticas que traduzem
de forma diferente:
```tsx
t({ message: 'Open', context: 'door' });
t({ message: 'Open', context: 'window' });
<Trans context="card-title">Untitled</Trans>
```
Strings contextuais aparecem nos catálogos de configurações regionais como um grupo indexado pelo seu
contexto: `"\<context>": { "\<message>": "\<translation>" }`. Os rótulos do manifesto são
contextuais por definição — cada um traz o papel de metadados que desempenha
(`objectMetadata.labelSingular`, `fieldMetadata.label`, `view.name`, …), por isso
a mesma palavra pode ser traduzida de forma diferente como nome de objeto e como nome de visualização.
## Extraindo e traduzindo
Execute o comando extracto a partir do diretório do seu aplicativo:
```bash
twenty dev:translations-extract # collect strings into locales/en.json
twenty dev:translations-extract --locale fr-FR # also scaffold a target locale
```
A extração coleta tanto os rótulos do seu manifesto quanto as strings de `t()`/`msg()`/`Trans`
no código-fonte do seu front-component em `locales/\<locale>.json`, indexadas pela
string de origem. Preencha as traduções:
```json
// locales/fr-FR.json
{
"Loading postcard…": "Chargement de la carte…",
"Hi {name}": "Bonjour {name}",
"Saved {count} cards": "{count} cartes enregistrées",
"door": {
"Open": "Ouvrir"
},
"objectMetadata.labelSingular": {
"Postcard": "Carte postale"
}
}
```
Strings sem contexto são entradas simples; strings contextuais — incluindo cada rótulo de manifesto — são aninhadas sob sua chave de contexto.
Espaços reservados como `{name}` são substituídos no tempo de execução — mantenha-os na tradução
. Qualquer cadeia de caracteres deixada vazia retorna ao texto de origem.
## Como ele funciona
`twenty dev:build` compila os catálogos e fornece o idioma correto para o
usuário atual: os rótulos do manifesto são resolvidos no lado do servidor e os catálogos de componentes de front-end
são incorporados em cada pacote de componente. No tempo de execução, um componente lê a localidade
de seu contexto de execução (o idioma atual do host) e resolve
cada seqüência de caracteres contra seu catálogo, voltando à fonte quando uma tradução
estiver faltando. Mudar o idioma no host faz com que as strings de `Trans` e
`useTranslate().t` sejam renderizadas novamente em tempo real.
Os rótulos do manifesto são compilados a cada sincronização — `twenty dev:build`, `twenty apply`
e também durante a observação contínua de `twenty dev` — portanto, editar um arquivo de localidade e
sincronizar é suficiente para ver o novo texto. Os catálogos de componentes do front-end são incorporados a
cada pacote de componentes no momento da compilação, portanto, alterar um deles ainda exige
executar novamente `twenty dev:build` (e reimplantar).
O conteúdo de texto (children) de `Trans` pode se estender por várias linhas — os espaços em branco são colapsados da
mesma forma que o JSX os colapsa, portanto, ambos são extraídos para a chave `Welcome back`:
```tsx
<Trans>Welcome back</Trans>
<Trans>
Welcome
back
</Trans>
```

View File

@@ -0,0 +1,81 @@
---
title: 5. An AI agent
icon: robot
description: Deixe um agente gerar documentos a partir de um chat, usando sua ferramenta.
---
Porque `generate-document` é exposto como uma **ferramenta**, um agente de IA pode chamá-lo.
Vamos adicionar um agente e uma habilidade para que os usuários possam dizer apenas *"generate a proposal for
Jeffery Griffin"*.
## A habilidade
A [skill](/l/pt/developers/extend/apps/logic/skills-and-agents) é reutilizável
instruções — conhecimento que anexa a agentes. Nós ensinamos o modelo como usar
a ferramenta.
```ts filename="src/skills/document-drafting.skill.ts"
import { defineSkill } from 'twenty-sdk/define';
export default defineSkill({
universalIdentifier: DOCUMENT_SKILL_UNIVERSAL_IDENTIFIER,
name: 'document-drafting',
label: 'Document drafting',
icon: 'IconFileText',
content: [
'To generate a document, call the `generate-document` tool with:',
'- `templateId`: the id of the document template to use.',
'- `recordId`: the id of the Person or Company the document is for.',
'',
'If the user names a template or person instead of an id, find the record first,',
'then pass its id. Make sure the template target matches the record type.',
].join('\n'),
});
```
## O agente
Um [agent](/l/pt/developers/extend/apps/logic/skills-and-agents) emparelha um prompt com um modelo
. Defina `responseFormat` explicitamente para evitar um aviso de construção.
```ts filename="src/agents/document-assistant.agent.ts"
import { defineAgent } from 'twenty-sdk/define';
export default defineAgent({
universalIdentifier: DOCUMENT_AGENT_UNIVERSAL_IDENTIFIER,
name: 'document-assistant',
label: 'Document Assistant',
description: 'Generates documents from your templates and CRM records.',
icon: 'IconFileText',
responseFormat: { type: 'text' },
prompt: [
'You are the Document Assistant for a CRM.',
'You help users generate personalized documents from reusable templates',
'and the data already in their CRM. Use the generate-document tool, and',
'always confirm what you created.',
].join(' '),
});
```
<Note>
O agente só pode chamar a ferramenta se o seu papel lhe permitir. Nós já definimos
`canAccessAllTools: true` and `canBeAssignedToAgents: true` on the app's role in
[Capítulo 2](/l/pt/developers/extend/apps/tutorials/document-generator/generating-documents#grant-it-access).
</Note>
## Experimente isso
Abra um chat com o **Assistente de Documentos** e peça para ele redigir um documento para uma
pessoa no seu CRM. Ele encontra o registro, chama `gerar documento` e relata
o documento que ele criou — que agora aparece na visão **Documentos**,
exatamente como o menu de comando e caminhos do fluxo de trabalho.
Esse é o benefício de expor a lógica como uma ferramenta: **uma função, muitas portas da frente** —
menu de comando, HTTP, passo do fluxo de trabalho e agora a linguagem natural.
**Após este passo:** o aplicativo está cheio de recursos e realmente útil. Hora de
lançar.
<Card title="Próximo: publicação →" icon="rocket" href="/desenvolvedores/adicionar/apps/tutorials/document-gerador/publicação">
Adicionar metadados do mercado e publicar.
</Card>

View File

@@ -0,0 +1,308 @@
---
title: 4. Construindo a interface
icon: table-columns
description: Views, navegação da barra lateral, um comando e componentes iniciais.
---
No momento, os objetos só podem ser acessados através de Configurações. Vamos dar ao aplicativo uma presença
real na UI: listar exibições, entradas da barra lateral, um clique
**Gerar documento** comando, um componente frontal de record-page para **visualizar** um documento
e uma guia de **editor** nativa para templates.
## Visualizações e navegação
A [view](/l/pt/developers/extend/apps/layout/views) é uma lista salva de um determinado objeto.
Um [item de menu de navegação](/l/pt/developers/extend/apps/layout/navigation-menu-items)
coloca essa visualização na barra lateral. O servidor já provisiona automaticamente a visualização padrão "All ..." de cada objeto, portanto, as visualizações que um app define são adicionais e selecionadas.
```ts filename="src/views/documents.view.ts"
import { defineView } from 'twenty-sdk/define';
export default defineView({
universalIdentifier: DOCUMENTS_VIEW_UNIVERSAL_IDENTIFIER,
name: 'All documents',
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconFile',
position: 0,
fields: [
{ universalIdentifier: DOCUMENTS_VIEW_NAME_FIELD_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: DOCUMENT_NAME_FIELD_UNIVERSAL_IDENTIFIER,
position: 0, isVisible: true, size: 280 },
{ universalIdentifier: DOCUMENTS_VIEW_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: DOCUMENT_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
position: 1, isVisible: true, size: 120 },
{ universalIdentifier: DOCUMENTS_VIEW_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: DOCUMENT_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
position: 2, isVisible: true, size: 200 },
],
});
```
```ts filename="src/navigation-menu-items/documents.navigation-menu-item.ts"
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define';
export default defineNavigationMenuItem({
universalIdentifier: DOCUMENTS_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
name: 'Documents',
icon: 'IconFile',
color: 'green',
position: 1,
type: NavigationMenuItemType.VIEW,
viewUniversalIdentifier: DOCUMENTS_VIEW_UNIVERSAL_IDENTIFIER,
});
```
Adicionar o mesmo par para modelos. Ambos agora aparecem na barra lateral:
<Frame caption="Documentos e Modelos na barra lateral, com o documento gerado listado.">
<img src="/images/docs/developers/extends/apps/document-generator/04-documents-view.png" alt="Visualização de documentos com um documento gerado" />
</Frame>
## Um componente frontal
A [front component](/l/pt/developers/extend/apps/layout/front-components) é um componente React
sandboxed dentro de Twenty. Ours lê o registro selecionado, carrega o modelo de pessoa
via `CoreApiClient`, e POSTs até a rota do último capítulo
.
```tsx filename="src/front-components/generate-document-form.front-component.tsx"
import { useEffect, useState } from 'react';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { RestApiClient } from 'twenty-client-sdk/rest';
import { defineFrontComponent } from 'twenty-sdk/define';
import { enqueueSnackbar, useSelectedRecordIds } from 'twenty-sdk/front-component';
const GenerateDocumentForm = () => {
const selectedRecordIds = useSelectedRecordIds();
const recordId = selectedRecordIds.length === 1 ? selectedRecordIds[0] : null;
const [templates, setTemplates] = useState<{ id: string; name: string }[]>([]);
const [templateId, setTemplateId] = useState('');
useEffect(() => {
new CoreApiClient()
.query({ documentTemplates: {
__args: { filter: { target: { eq: 'PERSON' } }, first: 100 },
edges: { node: { id: true, name: true } } } })
.then(({ documentTemplates }) => {
const list = documentTemplates?.edges?.map((e) => e.node) ?? [];
setTemplates(list);
if (list[0]) setTemplateId(list[0].id);
});
}, []);
const generate = async () => {
const res = await new RestApiClient().post<{ success: boolean }>(
'/s/documents/generate',
{ templateId, recordId },
);
await enqueueSnackbar({
message: res.success ? 'Document generated.' : 'Generation failed.',
variant: res.success ? 'success' : 'error',
});
};
// ...render a <select> of templates and a Generate button
};
export default defineFrontComponent({
universalIdentifier: GENERATE_DOCUMENT_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
name: 'generate-document-form',
component: GenerateDocumentForm,
});
```
<Warning>
Estilo com variáveis CSS inline (`var(--t-color-blue)`), não valores importados de
`25ui`. O SDK simula esse pacote durante a compilação, portanto, as importações em nível de módulo de
constantes de tema ficariam `undefined`. Veja o
[componente inteiro](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/front-components/generate-document-form.front-component.tsx).
</Warning>
## Um comando para abri-lo
Um [item de menu de comando](/l/pt/developers/extend/apps/layout/command-menu-items) com
`availabilityType: 'RECORD_SELECTION'` aparece quando uma pessoa é selecionada, e
abre o componente no painel lateral.
```ts filename="src/command-menu-items/generate-document.command-menu-item.ts"
import { defineCommandMenuItem, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: GENERATE_DOCUMENT_COMMAND_UNIVERSAL_IDENTIFIER,
label: 'Generate document',
availabilityType: 'RECORD_SELECTION',
availabilityObjectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
frontComponentUniversalIdentifier:
GENERATE_DOCUMENT_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
});
```
## Experimente todo o fluxo
Abra **Pessoas**, marque uma pessoa e pressione <kbd>⌘K</kbd> / <kbd>Ctrl K</kbd>.
"Gerar documento" aparece, marcado com seu aplicativo:
<Frame caption="O comando aparece quando uma pessoa é selecionada.">
<img src="/images/docs/developers/extends/apps/document-generator/06-command-menu.png" alt="Menu de comando com o documento gerado" />
</Frame>
Executá-lo — seu componente abre no painel lateral. Escolha um modelo, clique
**Gerar** e uma nova terra em **Documentos**.
<Frame caption="O componente inicial, carregando templates e gerando no clique.">
<img src="/images/docs/developers/extends/apps/document-generator/06b-front-component.png" alt="Gerar painel lateral do documento" />
</Frame>
Cada documento gerado grava o seu aplicativo como seu autor:
<Frame caption="Criado pelo Gerador do Documento: Status Gerado.">
<img src="/images/docs/developers/extends/apps/document-generator/05-document-record.png" alt="Um registro de documento gerado" />
</Frame>
## Pré-visualizar um documento em sua página de registro
Um componente frontal não é apenas para menus de comando. Você pode montar um como uma \*\*aba de uma página de registro
. Vamos adicionar uma aba *Pré-visualização* ao registro de documento que renderiza o corpo
Markdown como uma página polida e impressa.
O componente lê o id de registro atual de seu contexto de execução, carrega o documento
e o renderiza. Componentes frontais executados em uma **sandbox** que só permite uma lista
branca de tags HTML — injeção HTML bruta (`dangerouslySetInnerHTML`) e
`\<style>` estão bloqueados — então renderizamos o Markdown como elementos React com estilos inline
através de um pequeno [`Markdown`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/utils/markdown-to-react.tsx)
helper.
```tsx filename="src/front-components/document-viewer.front-component.tsx"
import { CoreApiClient } from 'twenty-client-sdk/core';
import { RestApiClient } from 'twenty-client-sdk/rest';
import { defineFrontComponent } from 'twenty-sdk/define';
import { useFrontComponentExecutionContext } from 'twenty-sdk/front-component';
import { Markdown } from 'src/utils/markdown-to-react';
const DocumentViewer = () => {
const recordId = useFrontComponentExecutionContext((c) => c.recordId ?? null);
// ...load { content, file } for recordId, then derive the links:
const pdfUrl = document.file?.[0]?.url;
const webUrl = new RestApiClient().resolveUrl('/s/documents/view', {
query: { id: recordId },
});
// Render the template body, plus quick links to the web page and the PDF.
// Links open in a new tab so they don't navigate the embedded component.
return (
<div style={styles.scroll}>
<div style={styles.actions}>
<a style={styles.actionLink} href={webUrl} target="_blank" rel="noopener noreferrer">
Open web page
</a>
{pdfUrl ? (
<a style={styles.actionLink} href={pdfUrl} target="_blank" rel="noopener noreferrer">
Download PDF
</a>
) : null}
</div>
<div style={styles.paper}>
<div style={styles.body}>
<Markdown content={document.content} />
</div>
</div>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: DOCUMENT_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
name: 'document-viewer',
component: DocumentViewer,
});
```
Monte-o com um [layout da página](/l/pt/developers/extend/apps/layout/page-layouts). Um
`RECORD_PAGE` layout adiciona abas à visualização do registro de um objeto; uma aba com um único
`FRONT_COMPONENT` widget hospeda o componente em largura total:
```ts filename="src/page-layouts/document-record.page-layout.ts"
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
export default definePageLayout({
universalIdentifier: DOCUMENT_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
name: 'Document record page',
type: 'RECORD_PAGE',
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
tabs: [{
universalIdentifier: DOCUMENT_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
title: 'Preview',
icon: 'IconEye',
position: 50,
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
widgets: [{
universalIdentifier: DOCUMENT_PAGE_LAYOUT_WIDGET_UNIVERSAL_IDENTIFIER,
title: 'Document preview',
type: 'FRONT_COMPONENT',
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier: DOCUMENT_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
}],
}],
});
```
Abra qualquer documento — uma guia **Pré-visualizar** renderiza belíssima, com links para a página web compartilhável de
e o PDF:
<Frame caption="A aba de Pré-visualização renderiza o documento com estilos embutidos, mais links rápidos.">
<img src="/images/docs/developers/extends/apps/document-generator/09-document-viewer.png" alt="Visualizador de documentos do componente frontal em uma aba de registros" />
</Frame>
## Edite um modelo com o editor de texto rico
Os templates não precisam de um componente personalizado. Como o `body` é um campo
`RICH_TEXT`, Twenty já fornece um editor de rich text completo para ele — o
mesmo que os objetos padrão Note e Task usam. Nós apenas a apresentamos na
página de registro de template.
Adicione uma aba com um widget `FIELD` no modo de exibição `EDITOR`, apontando para o campo `body`
através de `fieldMetadataId`:
```ts filename="src/page-layouts/template-record.page-layout.ts"
{
universalIdentifier: TEMPLATE_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
title: 'Template',
position: 1,
layoutMode: PageLayoutTabLayoutMode.GRID,
widgets: [{
universalIdentifier: TEMPLATE_PAGE_LAYOUT_WIDGET_UNIVERSAL_IDENTIFIER,
title: 'Template',
type: 'FIELD',
position: { layoutMode: PageLayoutTabLayoutMode.GRID, row: 0, column: 0, rowSpan: 6, columnSpan: 12 },
configuration: {
configurationType: 'FIELD',
fieldMetadataId: TEMPLATE_BODY_FIELD_UNIVERSAL_IDENTIFIER,
fieldDisplayMode: 'EDITOR',
},
}],
}
```
Um campo `RICH_TEXT` armazena tanto o JSON de blocos do editor quanto uma
projeção em Markdown. O pipeline de geração lê essa projeção Markdown, então
espaços reservados, o PDF, e a página da web compartilhável continuam funcionando inalterado —
veja o
[`template-record. leiaute-idade.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/page-layouts/template-record.page-layout.ts).
Widgets de `FIELD` também oferecem suporte a outros modos de exibição e opções, incluindo
a listagem de registros de uma relação aninhada a dois níveis de distância — veja a
[referência do widget de campo](/l/pt/developers/extend/apps/layout/page-layouts#field-widgets).
Agora editores escrevem modelos em um editor de texto rico:
<Frame caption="A aba Modelo: editor de texto rico nativo de 20 vinculado ao campo de corpo.">
<img src="/images/docs/developers/extends/apps/document-generator/10-template-editor.png" alt="Registro de modelo com a aba de editor de texto rico nativo" />
</Frame>
**Após este passo:** os documentos pré-visualizar lindo e os templates são editáveis
dentro do aplicativo. Em seguida, deixe um atendente da IA gerá-los a partir de um chat.
<Card title="Próximo: um atendente de IA →" icon="robot" href="/desenvolvedores/extend/apps/tutorials/document-generator/ai-agent">
Adicione um agente e uma habilidade que chama sua ferramenta.
</Card>

View File

@@ -0,0 +1,135 @@
---
title: 1. Modelo de dados
icon: database
description: Modela documentos e templates com objetos, campos e uma relação.
---
Nosso aplicativo precisa de dois objetos personalizados: **modelos de documentos** (o que escrever) e
**documentos** (o resultado gerado). Vamos defini-los.
Crie cada arquivo de entidade com o CLI — gera uma pasta UUID válida e
correta para você:
```bash filename="Terminal"
yarn twenty dev:add object
```
Abaixo nós mostramos os arquivos concluídos.
<Note>
Todo `*_UNIVERSAL_IDENTIFIER` constante vive em
`src/constants/universal-identifiers.ts` e é importado onde usado. Os trechos
abaixo omitem essas importações por brevidade — mantenha-as em seus próprios arquivos.
</Note>
## O objeto modelo
Um template tem um `nome`, um `body` com `{{placeholders}}`, e um `alvo` que
diz se é escrito para uma pessoa ou para uma empresa. O `body` é um campo
`RICH_TEXT`, então Vinte dá um editor completo de texto rico.
```ts filename="src/objects/document-template.object.ts"
import { defineObject, FieldType } from 'twenty-sdk/define';
export default defineObject({
universalIdentifier: DOCUMENT_TEMPLATE_OBJECT_UNIVERSAL_IDENTIFIER,
nameSingular: 'documentTemplate',
namePlural: 'documentTemplates',
labelSingular: 'Document template',
labelPlural: 'Document templates',
icon: 'IconFileText',
labelIdentifierFieldMetadataUniversalIdentifier:
TEMPLATE_NAME_FIELD_UNIVERSAL_IDENTIFIER,
fields: [
{ universalIdentifier: TEMPLATE_NAME_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.TEXT, name: 'name', label: 'Name', icon: 'IconAbc' },
{ universalIdentifier: TEMPLATE_BODY_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.RICH_TEXT, name: 'body', label: 'Body', icon: 'IconFileText',
description: 'Use {{placeholders}} like {{name.firstName}} or {{jobTitle}}.' },
{ universalIdentifier: TEMPLATE_TARGET_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.SELECT, name: 'target', label: 'Target', icon: 'IconTarget',
defaultValue: `'PERSON'`,
options: [
{ id: TEMPLATE_TARGET_OPTION_PERSON_UNIVERSAL_IDENTIFIER,
value: 'PERSON', label: 'Person', color: 'blue', position: 0 },
{ id: TEMPLATE_TARGET_OPTION_COMPANY_UNIVERSAL_IDENTIFIER,
value: 'COMPANY', label: 'Company', color: 'green', position: 1 },
] },
],
});
```
<Warning>
`SELECT` opção **valores** deve ser `UPPER_CASE` (`PERSON`, não `person`), e o
`defaultValue` está entre aspas extras: `` `'PERSON'` ``. A `etiqueta` é o que
usuários veem.
</Warning>
## O objeto do documento
O documento gerado armazena o `conteúdo` renderizado e um `estado`. Defina
da mesma forma, com uma seleção `status` de `DRAFT` / `GENERATED`. Arquivo completo:
[`document.object.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/objects/document.object.ts).
## Vinculando eles com uma relação
Cada documento deve apontar para o modelo de onde veio. Relações são
**bidirecionais** — você define ambos os lados, cada um em seu próprio arquivo de campo.
```ts filename="src/fields/document-template-relation.field.ts"
import { defineField, FieldType, OnDeleteAction, RelationType } from 'twenty-sdk/define';
// The "many" side: each document belongs to one template.
export default defineField({
universalIdentifier: DOCUMENT_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'template',
label: 'Template',
relationTargetObjectMetadataUniversalIdentifier:
DOCUMENT_TEMPLATE_OBJECT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier:
TEMPLATE_DOCUMENTS_FIELD_UNIVERSAL_IDENTIFIER,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'templateId',
},
});
```
O outro lado (`template-documents-relation.field.ts`) é um campo
`RelationType.ONE_TO_MANY` chamado `documentos` que aponta para o lado oposto.
Ver [Relations](/l/pt/developers/extend/apps/data/relations) para o padrão completo.
## Veja em Vinte
Com `yarn 20 dev` executando, abra **Settings → Data model**. Ambos os objetos
aparecem, marcados com seu aplicativo.
<Frame caption="Ambos os objetos personalizados, pertencentes ao aplicativo Gerador de Documentos.">
<img src="/images/docs/developers/extends/apps/document-generator/01-data-model.png" alt="Configurações do modelo de dados mostrando documentos e modelos de documento" />
</Frame>
Crie um modelo para testar com — nomeie-a *proposta de vendas*, defina **destino** para
*Personagem*, e cole um corpo com alguns espaços reservados:
```text
Dear {{name.firstName}} {{name.lastName}},
As {{jobTitle}} at {{company.name}}, we think you'll love our product.
Best,
The Team
```
<Frame caption="Um registro de template. O corpo mantém seus espaços reservados até que um documento seja gerado.">
<img src="/images/docs/developers/extends/apps/document-generator/03-template-record.png" alt="Um registro de modelo de proposta de vendas com corpo de placeholder" />
</Frame>
**Após essa etapa:** você tem objetos `documentTemplate` e `documento`, ligados por
a relação e um modelo para gerar. Em seguida, a lógica que a preenche.
<Card title="Próximo: gerando documentos →" icon="bolt" href="/desenvolvedores/extend/apps/tutorials/documento-gerador/documentos">
Escreva a função lógica que preenche o template.
</Card>

View File

@@ -0,0 +1,239 @@
---
title: 2. Gerando documentos
icon: bolt
description: Uma função lógica, exposta como uma ferramenta de IA e uma ação de fluxo de trabalho.
---
Agora o núcleo: uma [função lógica](/l/pt/developers/extend/apps/logic/logic-functions)
que carrega um modelo e um registro, preenche os espaços reservados e salva um novo documento
.
Vamos escrever a lógica do negócio uma vez na forma de **manipulador** e então expô-la através de
vários gatilhos. Este capítulo conecta duas delas — uma **ferramenta de IA** e uma
**ação de fluxo de trabalho**.
## O auxiliar de renderização
Mantenha uma lógica pura em seu próprio arquivo, para que seja fácil de testar unidades. Este encolher os tokens de um record
em `{{dot.path}}` e substitui-los.
```ts filename="src/logic-functions/utils/render-template.ts"
const PLACEHOLDER_PATTERN = /\{\{\s*([\w.]+)\s*\}\}/g;
export const renderTemplate = (body: string, values: Record<string, string>) => {
const missingTokens = new Set<string>();
const content = body.replace(PLACEHOLDER_PATTERN, (_m, token: string) => {
const value = values[token];
if (value === undefined || value === '') { missingTokens.add(token); return ''; }
return value;
});
return { content, missingTokens: [...missingTokens] };
};
```
<Tip>
Como este arquivo não tem efeitos colaterais, você pode resolvê-lo com testes de unidade
(`yarn test:unit`). Ver [Testing](/l/pt/developers/extend/apps/operations/testing).
</Tip>
## O manipulador
O manipulador usa o [`CoreApiClient`](/l/pt/developers/extend/apps/logic/logic-functions)
para ler e escrever dados de CRM. Ele carrega o modelo, carrega o registro de destino, preenche
o corpo e cria um `documento`.
```ts filename="src/logic-functions/handlers/generate-document-handler.ts"
import { CoreApiClient } from 'twenty-client-sdk/core';
import { loadRecordValues } from 'src/logic-functions/utils/load-record-values';
import { renderTemplate } from 'src/logic-functions/utils/render-template';
export const generateDocumentHandler = async (
input: { templateId: string; recordId: string },
) => {
const client = new CoreApiClient();
// Use a filtered list query, not the singular lookup: the singular query
// throws when nothing matches, which would become a 500 instead of a 404.
const { documentTemplates } = await client.query({
documentTemplates: {
__args: { filter: { id: { eq: input.templateId } }, first: 1 },
edges: { node: { id: true, name: true, body: true, target: true } },
},
});
const documentTemplate = documentTemplates?.edges?.[0]?.node;
if (!documentTemplate?.id) return { success: false, status: 404, message: 'Template not found.' };
const record = await loadRecordValues(client, documentTemplate.target, input.recordId);
if (!record.found) return { success: false, status: 404, message: 'Record not found.' };
const { content, missingTokens } = renderTemplate(documentTemplate.body ?? '', record.values);
const { createDocument } = await client.mutation({
createDocument: {
__args: { data: {
name: `${documentTemplate.name} — ${record.displayName}`,
content, status: 'GENERATED', templateId: documentTemplate.id,
} },
id: true, name: true,
},
});
return { success: true, documentId: createDocument.id, content, missingTokens };
};
```
`loadRecordValues` executa uma consulta diferente para uma Pessoa vs. Uma Empresa e flattens
o resultado — veja
[`load-record-values.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/utils/load-record-values.ts).
## Expo-na como uma ferramenta e uma ação de fluxo de trabalho
Uma única `defineLogicFunction` pode carregar vários gatilhos. Aqui, `toolTriggerSettings`
faz com que seja chamável por agentes IA e `workflowActionTriggerSettings` o transforma em
passo do construtor de fluxo de trabalho. Ambos descrevem suas informações com um esquema JSON.
```ts filename="src/logic-functions/generate-document.ts"
import { defineLogicFunction } from 'twenty-sdk/define';
import { jsonSchemaToInputSchema } from 'twenty-sdk/logic-function';
import { GENERATE_DOCUMENT_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
import { generateDocumentHandler } from 'src/logic-functions/handlers/generate-document-handler';
import { generateDocumentInputSchema } from 'src/logic-functions/schemas/generate-document-input.schema';
export default defineLogicFunction({
universalIdentifier: GENERATE_DOCUMENT_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER,
name: 'generate-document',
description: 'Generate a document from a template and a CRM record.',
timeoutSeconds: 30,
toolTriggerSettings: {
inputSchema: generateDocumentInputSchema,
},
workflowActionTriggerSettings: {
label: 'Generate Document',
icon: 'IconFileText',
inputSchema: jsonSchemaToInputSchema(generateDocumentInputSchema),
outputSchema: [{ type: 'object', properties: {
success: { type: 'boolean' }, documentId: { type: 'string' },
} }],
},
handler: generateDocumentHandler,
});
```
O esquema de entrada é um esquema JSON simples que descreve `templateId` e `recordId` —
vê [`generate-document-input.schema.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/schemas/generate-document-input.schema.ts).
## Concede-o acesso
Funções lógicas são executadas como o papel do aplicativo. Ele precisa ler templates e registrar
e criar documentos, então permitir que em `src/roles/default-role.ts`:
```ts
export default defineApplicationRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Document Generator default role',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canAccessAllTools: true,
canBeAssignedToAgents: true,
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.UPLOAD_FILE],
});
```
`UPLOAD_FILE` permite que a função envie o PDF gerado na próxima seção.
Ver [Roles](/l/pt/developers/extend/apps/config/roles) para permissões refinadas.
## Anexar arquivo PDF real
Um campo de texto renderizado é útil, mas os usuários querem um documento real. Vamos gerar um
**PDF** e armazená-lo no registro como um arquivo para download.
Primeiro, dê ao objeto `documento` um campo `ARQUIVO` para segurar o PDF. Aplicativos enviam
para seus **próprios** campos de arquivos, então este campo é o que encaminha o upload:
```ts filename="src/objects/document.object.ts"
{
universalIdentifier: DOCUMENT_FILE_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.FILES,
name: 'file',
label: 'File',
icon: 'IconFileTypePdf',
universalSettings: { maxNumberOfValues: 1 },
}
```
Agora renderize esse PDF. Uma app é um projeto Node real, então você pode adicionar qualquer pacote npm
que você precisa e importá-lo como em qualquer outro lugar. Nós usamos **[pdf-lib](https://pdf-lib.js.org/)**
para desenhar o PDF e **[marked](https://marked.js.org/)** para analisar o corpo do Markdown
— a CLI os instala no tempo de execução da função para você:
```bash filename="Terminal"
yarn add pdf-lib marked
```
O auxiliar completo é
[`generate-document-pdf.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/utils/generate-document-pdf.ts).
Ele analisa o Markdown em tokens com `marcado. exer`, então as envia com
pdf-lib: títulos reais, execuções **bold**/*italic*, listas de marcadores e numerados,
bloqueios e regras — uma renderização A4 polida e multi-página do modelo
em si, ao invés de uma parede de texto.
<Frame caption="O PDF: tipografia real e formatação Markdown, renderizando o corpo do modelo.">
<img src="/images/docs/developers/extends/apps/document-generator/07b-generated-pdf.png" alt="Um PDF gerado polido e comercializável" />
</Frame>
<Note>
As fontes internas do pdf-lib usam codificação WinAnsi, portanto os acentos da Europa Ocidental são exibidos
por padrão; o utilitário mapeia aspas tipográficas e travessões e descarta caracteres que
não consegue codificar. Renderizar scripts não-latinos (chinês, árabe, cirílico) significaria que
incorporando uma fonte Unicode.
</Note>
Em seguida, carregue-a e armazene a referência no registro. Rotas `uploadFile` bytes
para o campo de arquivos de propriedade; o `id` retornado é o que você salva:
```ts filename="src/logic-functions/handlers/generate-document-handler.ts"
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { generateDocumentPdf } from 'src/logic-functions/utils/generate-document-pdf';
const documentName = `${documentTemplate.name} — ${record.displayName}`;
const bytes = await generateDocumentPdf(documentName, content);
const fileName = 'proposal.pdf';
const uploaded = await new MetadataApiClient().uploadFile(
Buffer.from(bytes),
fileName,
'application/pdf',
DOCUMENT_FILE_FIELD_UNIVERSAL_IDENTIFIER,
);
await client.mutation({
updateDocument: {
__args: {
id: documentId,
data: { file: [{ fileId: uploaded.id, label: fileName }] },
},
id: true,
},
});
```
O documento gerado agora carrega um PDF:
<Frame caption="O PDF gerado, armazenado no campo Arquivo do documento.">
<img src="/images/docs/developers/extends/apps/document-generator/08-document-with-pdf.png" alt="Um registro de documento com um arquivo PDF gerado" />
</Frame>
<Note>
`uploadFile` destina-se apenas a campos de arquivos **propriedade do aplicativo** (então o upload sempre requer um aplicativo
que possui o campo, mais o sinalizador de papéis `UPLOAD_FILE`). É por isso que o PDF
fica no campo `file` do próprio registro - o mesmo padrão que o
[call-recorder app](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/public/call-recorder)
usa para gravações.
</Note>
**Após este passo:** cada documento gerado possui um PDF real e para download. Mas
nada pode *chamar* o gerador a partir da interface de usuário — por isso precisamos de uma rota HTTP.
<Card title="Próximo: Rotas HTTP →" icon="globo" href="/desenvolvedores/extend/apps/tutorials/document-gerator/http-routes">
Servir a função sobre HTTP e renderizar documentos como páginas da web.
</Card>

View File

@@ -0,0 +1,149 @@
---
title: 3. Rotas HTTP
icon: globe
description: Acionar a função sobre HTTP e renderizar documentos como páginas da web.
---
O mesmo manipulador também pode responder solicitações HTTP. Vamos adicionar duas rotas:
* um terminal **POST** aponta as chamadas da UI para gerar um documento e
* um endpoint de **GET** público que renderiza um documento como uma página web impressa.
Ambos usam `httpRouteTriggerSettings`. As rotas do aplicativo são acessadas com um caminho `/s/`; em instâncias locais
e auto-hospedadas, esse caminho é servido pelo seu servidor Twenty
(por exemplo, `http://localhost:2020/s/documents/generate`). No Twenty Cloud, `RestApiClient`
resolve o caminho por meio do domínio de funções do espaço de trabalho.
## Rota POST - gerar sob demanda
Isso reusa `generateDocumentHandler`, então não há lógica para repetir — apenas um adaptador
fino que lê o corpo da solicitação.
```ts filename="src/logic-functions/generate-document-route.ts"
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { Response } from 'twenty-sdk/logic-function';
import { generateDocumentHandler } from 'src/logic-functions/handlers/generate-document-handler';
const handler = async (event: RoutePayload): Promise<Response> => {
const body = event.body as Record<string, unknown> | null;
const result = await generateDocumentHandler({
templateId: (body?.templateId as string) ?? '',
recordId: (body?.recordId as string) ?? '',
});
// Map the handler's failure reason onto a real HTTP status (400/404/500)
// instead of always returning 200.
return new Response(JSON.stringify(result), {
status: result.success ? 200 : (result.status ?? 400),
headers: { 'Content-Type': 'application/json' },
});
};
export default defineLogicFunction({
universalIdentifier: GENERATE_DOCUMENT_ROUTE_UNIVERSAL_IDENTIFIER,
name: 'generate-document-route',
timeoutSeconds: 30,
handler,
httpRouteTriggerSettings: {
path: '/documents/generate',
httpMethod: 'POST',
isAuthRequired: true,
},
});
```
O manipulador compartilhado retorna um 'status' sugerido durante a falha, então a rota pode
responder com um código `4xx`/`5xx` apropriado. `isAuthRequired: true` significa que o chamador
deve apresentar um token válido — o componente frontal no próximo capítulo passa o token de acesso do usuário
automaticamente.
## Via GET - renderizar como uma página da web
Para retornar HTML em vez de JSON, encapsule o corpo em um `Response` com um cabeçalho
`Content-Type`. Esta rota é pública (`isAuthRequired: false`) para que um documento
gerado possa ser compartilhado como um link.
```ts filename="src/logic-functions/view-document.ts"
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { Response } from 'twenty-sdk/logic-function';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { documentHtmlPage } from 'src/utils/render-document';
const htmlResponse = (html: string, status = 200): Response =>
new Response(html, { status, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
const handler = async (event: RoutePayload): Promise<Response> => {
const documentId = event.queryStringParameters?.id;
if (!documentId) {
return htmlResponse(documentHtmlPage('Missing document id', 'Provide ?id=<documentId>.'), 400);
}
// Filtered list query so an unknown id renders a clean 404 page instead of throwing.
const { documents } = await new CoreApiClient().query({
documents: {
__args: { filter: { id: { eq: documentId } }, first: 1 },
edges: { node: { id: true, name: true, content: true } },
},
});
const document = documents?.edges?.[0]?.node;
if (!document?.id) {
return htmlResponse(documentHtmlPage('Document not found', `No document with id ${documentId}.`), 404);
}
return htmlResponse(documentHtmlPage(document.name ?? 'Document', document.content ?? ''));
};
export default defineLogicFunction({
universalIdentifier: VIEW_DOCUMENT_ROUTE_UNIVERSAL_IDENTIFIER,
name: 'view-document',
timeoutSeconds: 15,
handler,
httpRouteTriggerSettings: {
path: '/documents/view',
httpMethod: 'GET',
isAuthRequired: false,
},
});
```
`documentHtmlPage` renderiza o corpo do Markdown para HTML (com [marked](https://marked.js.org/),
sanitizado) e o coloca em um limpo, página imprimível que mostra apenas o conteúdo do modelo- a mesma aparência que o PDF e a visualização no aplicativo.
[Veja o ajudante](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/utils/render-document.ts).
## Experimente isso
Com um modelo e uma pessoa em seu espaço de trabalho, chame a rota (pegue um token do
**Settings → APIs & Webhooks**):
```bash filename="Terminal"
curl -X POST http://localhost:2020/s/documents/generate \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"templateId":"<templateId>","recordId":"<personId>"}'
# → {"success":true,"documentId":"...","content":"Dear Jeffery Griffin, ..."}
```
Abra o documento retornado no seu navegador:
```
http://localhost:2020/s/documents/view?id=<documentId>
```
<Frame caption="A rota GET pública renderiza o documento como uma página impressa.">
<img src="/images/docs/developers/extends/apps/document-generator/07-rendered-document.png" alt="Uma página renderizada do documento web" />
</Frame>
<Tip>
Você também pode transmitir logs de uma função enquanto está testando com
`yarn twenty dev:function:logs`, ou invocá-los diretamente com
`yarn twenty dev:function:exec`.
</Tip>
**Após esse passo:** o aplicativo pode gerar documentos via HTTP e servi-los como páginas
web. Agora vamos torná-lo utilizável sem `curl`.
<Card title="Próximo: Construindo a interface do usuário →" icon="table-columns" href="/desenvolvedores/extend/apps/tutorials/document-generator/building-the-ui">
Exibir, navegação, um comando e um componente inicial.
</Card>

View File

@@ -0,0 +1,66 @@
---
title: "Tutorial: Gerador de Documentos"
icon: wand-magic-sparkles
description: Crie um aplicativo Twenty real que gera documentos personalizados a partir dos seus dados de CRM.
---
Neste tutorial, você vai criar o **Gerador de Documentos** — um aplicativo que transforma modelos reutilizáveis
em documentos personalizados usando os dados que você já tem no seu CRM.
Escreva um modelo uma vez com `{{placeholders}}` e, em seguida, gere um documento preenchido para qualquer Pessoa ou Empresa com um clique — a partir do menu de comandos, de um agente de IA ou de um fluxo de trabalho.
<Frame caption="Um modelo, gerado para uma pessoa específica, aberto como uma página para impressão.">
<img src="/images/docs/developers/extends/apps/document-generator/07-rendered-document.png" alt="Um documento de proposta de vendas gerado" />
</Frame>
## O que você vai aprender
Cada capítulo adiciona uma capacidade. Ao final, você terá usado a maior parte do SDK.
| Capítulo | Capacidade | Referência |
| ----------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| [1. Modelo de Dados](/l/pt/developers/extend/apps/tutorials/document-generator/data-model) | Objetos, campos e uma relação | [Dados](/l/pt/developers/extend/apps/data/overview) |
| [2. Geração de documentos](/l/pt/developers/extend/apps/tutorials/document-generator/generating-documents) | Uma função lógica (ferramenta de IA + ação de fluxo de trabalho) que preenche um modelo Markdown e anexa um PDF finalizado | [Funções lógicas](/l/pt/developers/extend/apps/logic/logic-functions) |
| [3. Rotas HTTP](/l/pt/developers/extend/apps/tutorials/document-generator/http-routes) | Servindo JSON e uma página HTML compartilhável a partir de rotas | [Funções lógicas](/l/pt/developers/extend/apps/logic/logic-functions) |
| [4. Criando a UI](/l/pt/developers/extend/apps/tutorials/document-generator/building-the-ui) | Vistas, navegação, menu de comandos e componentes de front-end que pré-visualizam um documento e editam um modelo | [Layout](/l/pt/developers/extend/apps/layout/overview) |
| [5. Um agente de IA](/l/pt/developers/extend/apps/tutorials/document-generator/ai-agent) | Agente + habilidade | [Habilidades e Agentes](/l/pt/developers/extend/apps/logic/skills-and-agents) |
| [6. Publicação](/l/pt/developers/extend/apps/tutorials/document-generator/publishing) | Envie-o para o marketplace | [Publicação](/l/pt/developers/extend/apps/operations/publishing) |
## Pré-requisitos
Você deve ter concluído o [Quick Start](/l/pt/developers/extend/apps/getting-started/quick-start):
um servidor Twenty local em execução na porta `2020` e o CLI autenticado nele.
Se não, gere a estrutura e inicie um agora:
```bash filename="Terminal"
npx create-twenty-app@latest document-generator
cd document-generator
yarn twenty dev
```
<Note>
Prefere ler o código finalizado? O aplicativo completo está em
[`packages/twenty-apps/examples/document-generator`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/document-generator).
Cada trecho abaixo foi copiado de lá.
</Note>
## Como o aplicativo se encaixa
<Frame>
<img src="/images/docs/developers/extends/apps/document-generator/how-it-fits.svg" alt="Um modelo com placeholders é transformado em um documento finalizado com um PDF, acionado a partir do menu de comandos, de um agente de IA, de um fluxo de trabalho ou de um link compartilhável" />
</Frame>
Você escreve um **modelo** uma vez em um editor de texto rico, com `{{placeholders}}`. Escolher um
modelo e um registro de CRM preenche os placeholders e armazena um
**documento** finalizado (com um arquivo PDF). Todo o resto — o menu de comandos, o agente de IA, a etapa do fluxo de trabalho, o link compartilhável — é apenas uma forma diferente de acionar aquele único gerador.
## Mantenha este ciclo em execução
Deixe `yarn twenty dev` rodando em um terminal durante todo o tutorial. Toda vez que
você adicionar ou editar um arquivo em `src/`, ele será ressíncronizado com o seu servidor em alguns
segundos, para que você possa ver cada capacidade aparecer na UI conforme a constrói.
<Card title="Comece a construir →" icon="database" href="/l/pt/developers/extend/apps/tutorials/document-generator/data-model">
Capítulo 1: modele documentos e modelos de documento.
</Card>

View File

@@ -0,0 +1,137 @@
---
title: 6. Publicação
icon: rocket
description: Adicione metadados da loja e publique seu aplicativo.
---
Seu aplicativo funciona. O último passo é descrevê-lo para o mercado e publicar.
## Adicionar metadados de mercado
A [aplicação config](/l/pt/developers/extend/apps/config/application) transporta a identidade
que aparece no mercado: autor, categoria, logotipo e suporte links
. Coloque um logotipo em `public/` e faça referência a ele com `logo`.
```ts filename="src/application-config.ts"
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
displayName: 'Document Generator',
description:
'Create reusable document templates and generate personalized documents from your CRM records.',
logo: 'public/document-generator.svg',
author: 'Twenty',
category: 'Productivity',
websiteUrl: 'https://docs.twenty.com/l/pt/developers/extend/apps',
termsUrl: 'https://www.twenty.com/terms',
emailSupport: 'contact@twenty.com',
issueReportUrl: 'https://github.com/twentyhq/twenty/issues',
});
```
<Tip>
O papel padrão é declarado com `defineApplicationRole()` em seu próprio arquivo — você
não passa mais `defaultRoleUniversalIdentifier` aqui.
</Tip>
Também adicione a palavra-chave `vinte app` ao `package.json` para que o aplicativo seja detectável:
```json filename="package.json"
{ "keywords": ["twenty-app"] }
```
## Adicionar capturas de tela da galeria
Uma lista de mercado vende a si mesma com screenshots. Coloque alguns PNGs em
`public/gallery/` e faça referência a eles com `galleryImages` — eles são renderizados como uma
galeria na página de listagem, na ordem do array.
```ts filename="src/application-config.ts"
export default defineApplication({
// ...identity from above
galleryImages: [
'public/gallery/01-generated-document.png',
'public/gallery/02-command-menu.png',
'public/gallery/03-template-editor.png',
'public/gallery/04-documents.png',
],
});
```
<Tip>
Conduzir com o pagamento: fazer a primeira captura de tela o resultado finalizado (um documento
gerado), depois mostrar como é acionado e autorado. Use capturas
crocantes e de alta resolução — elas são a primeira coisa que um usuário vê.
</Tip>
Dê ao `README.md` o mesmo tratamento — é a página inicial em npm e GitHub.
Abra com a proposição de valor e uma captura de tela, liste os recursos do título,
e então mantenha os detalhes de compilação abaixo da dobra.
## Verifique antes de enviar
Executar os mesmos portões CI do portão:
```bash filename="Terminal"
yarn lint # oxlint
yarn typecheck # tsgo
yarn test:unit # unit tests
yarn twenty plan # preview the metadata diff
```
O plano mostra exatamente o que mudaria no servidor sem aplicar as alterações —
um bom último teste de sanidade. Veja
[Testing](/l/pt/developers/extend/apps/operations/testing) e
[Sincronização e recuperação](/l/pt/developers/extend/apps/operations/sync-and-recovery).
## Publicar
```bash filename="Terminal"
# Public app → npm (default)
yarn twenty app:publish
# Or deploy privately to a specific server's registry
yarn twenty app:publish --private -r <remote>
```
`app:publish` compila e publica para o npm por padrão; `--private` envia uma tarball
para um registro privado de vinte servidores. Para superfíciar um aplicativo publicado
no mercado de uma instância, acione uma sincronização de catálogo:
```bash filename="Terminal"
yarn twenty dev:catalog-sync -r <remote>
```
Detalhes completos e o checklist de lançamento:
[Publishing](/l/pt/developers/extend/apps/operations/publishing).
## Você construiu um aplicativo 🎉
Em seis capítulos você usou a maior parte da superfície SDK:
* **Objetos, campos e uma relação** para modelar os dados
* Uma **função lógica** exposta como uma **ferramenta de Inteligência**, uma **ação de fluxo de trabalho** e **rotas HTTP**
* **Exibir, navegação, um comando e um componente frontal** para a interface do usuário
* Um **agente + habilidade** para a geração de idioma natural
* **Metadados do Marketplace** e o fluxo de publicação
O aplicativo completo está em
[`packages/twenty-apps/examples/document-generator`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/document-generator).
## Para onde ir
<CardGroup cols={2}>
<Card title="Referência de dados" icon="database" href="/l/pt/developers/extend/apps/data/overview">
Todos os tipos de campo, relação e opção de índice.
</Card>
<Card title="Referência lógica" icon="bolt" href="/l/pt/developers/extend/apps/logic/overview">
Gatilhos Cron e database-event trigers, a loja chave-valor, conexões OAuth
</Card>
<Card title="Referência do layout" icon="table-columns" href="/l/pt/developers/extend/apps/layout/overview">
Layouts da página, widgets do painel e mais superfícies da UI.
</Card>
<Card title="Operações" icon="rocket" href="/l/pt/developers/extend/apps/operations/overview">
CLI, testes, controles remotos e CI.
</Card>
</CardGroup>

View File

@@ -0,0 +1,147 @@
---
title: APIs
description: Consulte e modifique seus dados de CRM programaticamente usando REST ou GraphQL.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
O Twenty foi desenvolvido para ser amigável ao desenvolvedor, oferecendo APIs poderosas que se adaptam ao seu modelo de dados personalizado. Oferecemos quatro tipos distintos de API para atender diferentes necessidades de integração.
## Abordagem Focada no Desenvolvedor
Twenty gera APIs especificamente para o seu modelo de dados:
* **Nenhum ID longo necessário**: Use os nomes dos seus objetos e campos diretamente nas endpoints
* **Objetos padrão e personalizados tratados igualmente**: Seus objetos personalizados recebem o mesmo tratamento de API que os incorporados
* **Endpoints dedicados**: Cada objeto e campo tem seu próprio endpoint de API
* **Documentação personalizada**: Gerada especificamente para o modelo de dados do seu workspace
<Note>
Sua documentação de API personalizada fica disponível em **Configurações → API & Webhooks** após criar uma chave de API. Como o Twenty gera APIs que correspondem ao seu modelo de dados personalizado, a documentação é exclusiva do seu workspace.
</Note>
## Os dois tipos de API
### Core API
Acessada em `/rest/` ou `/graphql/`
Trabalhe com seus **registros** (os dados):
* Criar, ler, atualizar e excluir Pessoas, Empresas, Oportunidades, etc.
* Consultar e filtrar dados
* Gerenciar relações de registros
### Metadata API
Acessada em `/rest/metadata/` ou `/metadata/`
Gerencie seu **workspace e modelo de dados**:
* Criar, modificar ou excluir objetos e campos
* Configurar as configurações do workspace
* Defina relacionamentos entre objetos
## REST vs GraphQL
As APIs Core e Metadata estão disponíveis nos formatos REST e GraphQL:
| Formato | Operações disponíveis |
| ----------- | --------------------------------------------------------------------------------- |
| **REST** | CRUD, operações em lote, upserts |
| **GraphQL** | Os mesmos + **upserts em lote**, consultas de relacionamento em uma única chamada |
Escolha com base nas suas necessidades — ambos os formatos acessam os mesmos dados.
## Endpoints de API
| Ambiente | URL base |
| ------------------ | ------------------------- |
| **Nuvem** | `https://api.twenty.com/` |
| **Auto-hospedado** | `https://{your-domain}/` |
## Autenticação
Toda solicitação de API requer uma chave de API no cabeçalho:
```
Authorization: Bearer YOUR_API_KEY
```
### Criar uma Chave de API
1. Vá para **Configurações → APIs & Webhooks**
2. Clique em **+ Criar chave**
3. Configurar:
* **Nome**: Nome descritivo para a chave
* **Data de expiração**: Quando a chave expira
4. Clique em **Salvar**
5. **Copie imediatamente** — a chave é exibida apenas uma vez
<VimeoEmbed videoId="928786722" title="Criando chave de API" />
<Warning>
Sua chave de API concede acesso a dados confidenciais. Não a compartilhe com serviços não confiáveis. Se for comprometida, desative-a imediatamente e gere uma nova.
</Warning>
### Atribuir uma função a uma chave de API
Para maior segurança, atribua uma função específica para limitar o acesso:
1. Vá para **Configurações → Membros → Funções**
2. Clique na função que deseja atribuir
3. Abra a aba de **Atribuição**
4. Em **Chaves de API**, clique em **+ Atribuir à chave de API**
5. Selecione a chave de API
A chave herdará as permissões dessa função. Veja [Permissões](/l/pt/user-guide/permissions-access/capabilities/permissions) para obter detalhes.
### Gerenciar Chaves de API
**Regenerar**: Configurações → APIs & Webhooks → Clique na chave → **Regenerar**
**Excluir**: Configurações → APIs & Webhooks → Clique na chave → **Excluir**
## Playground de API
Teste suas APIs diretamente no navegador com nosso playground integrado — disponível tanto para **REST** quanto para **GraphQL**.
### Acesse o Playground
1. Vá para **Configurações → APIs & Webhooks**
2. Crie uma chave de API (obrigatório)
3. Clique em **REST API** ou **GraphQL API** para abrir o playground
### O que você obtém
* **Documentação interativa**: Gerada para o seu modelo de dados específico
* **Testes ao vivo**: Execute chamadas de API reais no seu workspace
* **Explorador de esquema**: Navegue pelos objetos, campos e relacionamentos disponíveis
* **Construtor de solicitações**: Construa consultas com preenchimento automático
O playground reflete seus objetos e campos personalizados, portanto, a documentação está sempre precisa para o seu workspace.
## Operações em Lote
Tanto REST quanto GraphQL suportam operações em lote:
* **Tamanho do lote**: Até 60 registros por requisição
* **Operações**: Criar, atualizar e excluir vários registros
**Recursos exclusivos do GraphQL:**
* **Upsert em lote**: Criar ou atualizar em uma única chamada
* Use nomes de objetos no plural (por exemplo, `CreateCompanies` em vez de `CreateCompany`)
## Limites de taxa
As solicitações de API são limitadas para garantir a estabilidade da plataforma:
| Limite | Valor |
| ------------------- | ------------------------ |
| **Solicitações** | 100 chamadas por minuto |
| **Tamanho do lote** | 60 registros por chamada |
<Tip>
Use operações em lote para maximizar a taxa de transferência — processe até 60 registros em uma única chamada de API em vez de fazer solicitações individuais.
</Tip>

View File

@@ -0,0 +1,112 @@
---
title: Webhooks
description: Receba notificações em tempo real quando eventos ocorrerem no seu CRM.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
Os webhooks enviam dados para seus sistemas em tempo real quando eventos ocorrem no Twenty — sem necessidade de polling. Use-os para manter sistemas externos em sincronia, acionar automações ou enviar alertas.
## Criar Webhook
1. Vá para **Configurações → APIs & Webhooks → Webhooks**
2. Clique em **+ Criar webhook**
3. Insira a URL do seu webhook (deve ser publicamente acessível)
4. Clique em **Salvar**
O webhook é ativado imediatamente e começa a enviar notificações.
<VimeoEmbed videoId="928786708" title="Criando um webhook" />
### Gerenciar Webhooks
**Editar**: Clique no webhook → Atualizar URL → **Salvar**
**Excluir**: Clique no webhook → **Excluir** → Confirmar
## Eventos
O Twenty envia webhooks para estes tipos de eventos:
| Evento | Exemplo |
| ----------------------- | ---------------------------------------------------------- |
| **Registro criado** | `person.created`, `company.created`, `note.created` |
| **Registro atualizado** | `person.updated`, `company.updated`, `opportunity.updated` |
| **Registro excluído** | `person.deleted`, `company.deleted` |
Todos os tipos de evento são enviados para a URL do seu webhook. A filtragem de eventos pode ser adicionada em versões futuras.
## Formato do payload
Cada webhook envia um HTTP POST com um corpo JSON:
```json
{
"event": "person.created",
"data": {
"id": "abc12345",
"firstName": "Alice",
"lastName": "Doe",
"email": "alice@example.com",
"createdAt": "2025-02-10T15:30:45Z",
"createdBy": "user_123"
},
"timestamp": "2025-02-10T15:30:50Z"
}
```
| Campo | Descrição |
| ------------------- | ------------------------------------------------------ |
| `evento` | O que aconteceu (por exemplo, `person.created`) |
| `data` | O registro completo que foi criado/atualizado/excluído |
| `registro de Tempo` | Quando o evento ocorreu (UTC) |
<Note>
Responda com um **status HTTP 2xx** (200-299) para confirmar o recebimento. Respostas não 2xx são registradas como falhas de entrega.
</Note>
## Validação de Webhook
O Twenty assina cada solicitação de webhook por segurança. Valide as assinaturas para garantir que as solicitações sejam autênticas.
### Headers
| Cabeçalho | Descrição |
| ---------------------------- | ------------------------ |
| `X-Twenty-Webhook-Signature` | Assinatura HMAC SHA256 |
| `X-Twenty-Webhook-Timestamp` | Timestamp da solicitação |
### Etapas de validação
1. Obtenha o timestamp de `X-Twenty-Webhook-Timestamp`
2. Crie a string: `{timestamp}:{JSON payload}`
3. Calcule o HMAC SHA256 usando o segredo do seu webhook
4. Compare com `X-Twenty-Webhook-Signature`
### Exemplo (Node.js)
```javascript
const crypto = require("crypto");
const timestamp = req.headers["x-twenty-webhook-timestamp"];
const payload = JSON.stringify(req.body);
const secret = "your-webhook-secret";
const stringToSign = `${timestamp}:${payload}`;
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(stringToSign)
.digest("hex");
const isValid = expectedSignature === req.headers["x-twenty-webhook-signature"];
```
## Webhooks vs Fluxos de trabalho
| Método | Direção | Caso de uso |
| ------------------------------------------- | ------- | -------------------------------------------------------------------------------- |
| **Webhooks** | SAÍDA | Notificar automaticamente sistemas externos sobre qualquer alteração de registro |
| **Fluxo de trabalho + Solicitação HTTP** | SAÍDA | Enviar dados para fora com lógica personalizada (filtros, transformações) |
| **Gatilho de webhook de fluxo de trabalho** | ENTRADA | Receber dados no Twenty a partir de sistemas externos |
Para receber dados externos, consulte [Configurar um gatilho de Webhook](/l/pt/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger).

View File

@@ -0,0 +1,32 @@
---
title: Estender
description: Amplie a funcionalidade do Twenty com APIs, webhooks e aplicativos personalizados.
---
<Frame>
<img src="/images/user-guide/integrations/plug.png" alt="IA" />
</Frame>
## Visão geral
O Twenty foi projetado para ser extensível. Use nossas APIs, webhooks e o framework de aplicativos para integrar-se às suas ferramentas existentes e criar funcionalidades personalizadas.
## O que você pode fazer
* **APIs**: Consulte e modifique seus dados de CRM programaticamente usando REST ou GraphQL
* **Webhooks**: Receba notificações em tempo real quando eventos ocorrerem no Twenty
* **Apps**: Crie aplicativos personalizados que expandem as capacidades do Twenty
## Primeiros passos
<CardGroup cols={2}>
<Card title="APIs" icon="code" href="/l/pt/developers/extend/api">
Conecte-se ao Twenty programaticamente
</Card>
<Card title="Webhooks" icon="bell" href="/l/pt/developers/extend/webhooks">
Receba notificações de eventos em tempo real
</Card>
<Card title="Aplicativos" icon="puzzle-piece" href="/desenvolvedores/estend/apps/getting-started/quick-start">
Crie personalizações como código
</Card>
</CardGroup>

View File

@@ -0,0 +1,189 @@
---
title: OAuth
icon: key
description: Fluxo de código de autorização com PKCE e credenciais de cliente para acesso servidor a servidor.
---
A Twenty implementa OAuth 2.0 com código de autorização + PKCE para aplicações voltadas ao utilizador e credenciais de cliente para acesso servidor a servidor. Os clientes são registados dinamicamente via [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591) — sem configuração manual num painel.
## Quando usar OAuth
| Cenário | Método de autenticação |
| -------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| Scripts internos, automatização | [Chave de API](/l/pt/developers/extend/api#authentication) |
| Aplicação externa atuando em nome de um utilizador | **OAuth — Código de Autorização** |
| Servidor a servidor, sem contexto de utilizador | **OAuth — Credenciais do Cliente** |
| Aplicação Twenty com extensões de UI | [Aplicações](/l/pt/developers/extend/apps/getting-started/quick-start) (OAuth é tratado automaticamente) |
## Registar um cliente
A Twenty suporta **registo dinâmico de clientes** conforme [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591). Sem necessidade de configuração manual — registe programaticamente:
```bash
POST /oauth/register
Content-Type: application/json
{
"client_name": "My Integration",
"redirect_uris": ["https://myapp.com/callback"],
"grant_types": ["authorization_code"],
"token_endpoint_auth_method": "client_secret_post"
}
```
**Resposta:**
```json
{
"client_id": "abc123",
"client_secret": "secret456",
"client_name": "My Integration",
"redirect_uris": ["https://myapp.com/callback"]
}
```
<Warning>
Guarde o `client_secret` com segurança — não poderá ser recuperado mais tarde.
</Warning>
## Escopos
| Escopo | Acesso |
| -------- | ------------------------------------------------------- |
| `api` | Acesso total de leitura/escrita às APIs Core e Metadata |
| `perfil` | Leia as informações do perfil do utilizador autenticado |
Solicite escopos como uma cadeia separada por espaços: `scope=api profile`
## Fluxo de Código de Autorização
Use este fluxo quando a sua aplicação atuar em nome de um utilizador da Twenty.
### 1. Redirecione o utilizador para autorizar
```
GET /oauth/authorize?
client_id=YOUR_CLIENT_ID&
response_type=code&
redirect_uri=https://myapp.com/callback&
scope=api&
state=random_state_value&
code_challenge=CHALLENGE&
code_challenge_method=S256
```
| Parâmetro | Obrigatório | Descrição |
| ----------------------- | ----------- | ------------------------------------------------------------------- |
| `client_id` | Sim | O seu ID de cliente registado |
| `response_type` | Sim | Deve ser `code` |
| `redirect_uri` | Sim | Deve corresponder a um URI de redirecionamento registado |
| `scope` | Não | Escopos separados por espaços (predefinido para `api`) |
| `estado` | Recomendado | Cadeia aleatória para prevenir ataques CSRF |
| `code_challenge` | Recomendado | Desafio PKCE (hash SHA-256 do verificador, codificado em base64url) |
| `code_challenge_method` | Recomendado | Deve ser `S256` ao usar PKCE |
O utilizador vê um ecrã de consentimento e aprova ou nega o acesso.
### 2. Trate o callback
Após a autorização, a Twenty redireciona de volta para o seu `redirect_uri`:
```
https://myapp.com/callback?code=AUTH_CODE&state=random_state_value
```
Verifique se `state` corresponde ao que enviou.
### 3. Troque o código por tokens
```bash
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=AUTH_CODE&
redirect_uri=https://myapp.com/callback&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
code_verifier=YOUR_PKCE_VERIFIER
```
**Resposta:**
```json
{
"access_token": "eyJhbG...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBh..."
}
```
### 4. Use o token de acesso
```bash
GET /rest/companies
Authorization: Bearer ACCESS_TOKEN
```
### 5. Atualize quando expirar
```bash
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&
refresh_token=YOUR_REFRESH_TOKEN&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET
```
## Fluxo de Credenciais do Cliente
Para integrações servidor a servidor sem interação do utilizador:
```bash
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
scope=api
```
O token retornado tem acesso ao nível do espaço de trabalho, não vinculado a nenhum utilizador específico.
## Descoberta do servidor
A Twenty publica a sua configuração OAuth num endpoint padrão de descoberta:
```
GET /.well-known/oauth-authorization-server
```
Isto retorna todos os endpoints, tipos de concessão suportados, escopos e capacidades — útil para criar clientes OAuth genéricos.
## Resumo de endpoints da API
| Endpoint | Finalidade |
| ----------------------------------------- | ----------------------------------- |
| `/.well-known/oauth-authorization-server` | Descoberta de metadados do servidor |
| `/oauth/register` | Registo dinâmico de cliente |
| `/oauth/authorize` | Autorização do utilizador |
| `/oauth/token` | Troca e atualização de tokens |
| Ambiente | URL base |
| ------------------ | ------------------------ |
| **Nuvem** | `https://api.twenty.com` |
| **Auto-hospedado** | `https://{your-domain}` |
## OAuth vs Chaves de API
| | Chaves API | OAuth |
| ----------------------------- | ------------------------------------ | ------------------------------------------------ |
| **Configuração** | Gerar em Configurações | Registar um cliente, implementar o fluxo |
| **Contexto do utilizador** | Nenhum (nível de espaço de trabalho) | Permissões de um utilizador específico |
| **Melhor para** | Scripts, ferramentas internas | Aplicações externas, integrações multiutilizador |
| **Rotação de tokens** | Manual | Automática via tokens de atualização |
| **Acesso baseado em escopos** | Acesso total à API | Granular por escopos |

View File

@@ -0,0 +1,117 @@
---
title: Webhooks
icon: satellite-dish
description: Receba notificações quando os registros mudarem — HTTP POST para o seu endpoint a cada criação, atualização ou exclusão.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
Twenty envia um HTTP POST para sua URL sempre que um registro é criado, atualizado ou excluído. Todos os tipos de objeto estão cobertos, incluindo objetos personalizados.
## Criar Webhook
1. Vá para **Configurações → APIs & Webhooks → Webhooks**
2. Clique em **+ Criar webhook**
3. Insira a URL do seu webhook (deve ser publicamente acessível)
4. Clique em **Salvar**
O webhook é ativado imediatamente e começa a enviar notificações.
<VimeoEmbed videoId="928786708" title="Criando um webhook" />
### Gerenciar Webhooks
**Editar**: Clique no webhook → Atualizar URL → **Salvar**
**Excluir**: Clique no webhook → **Excluir** → Confirmar
## Eventos
O Twenty envia webhooks para estes tipos de eventos:
| Evento | Exemplo |
| ----------------------- | ---------------------------------------------------------- |
| **Registro criado** | `person.created`, `company.created`, `note.created` |
| **Registro atualizado** | `person.updated`, `company.updated`, `opportunity.updated` |
| **Registro excluído** | `person.deleted`, `company.deleted` |
Todos os tipos de evento são enviados para a URL do seu webhook. A filtragem de eventos pode ser adicionada em versões futuras.
## Formato do payload
Cada webhook envia um HTTP POST com um corpo JSON:
```json
{
"event": "person.created",
"data": {
"id": "abc12345",
"firstName": "Alice",
"lastName": "Doe",
"email": "alice@example.com",
"createdAt": "2025-02-10T15:30:45Z",
"createdBy": "user_123"
},
"timestamp": "2025-02-10T15:30:50Z"
}
```
| Campo | Descrição |
| ----------- | ------------------------------------------------------ |
| `event` | O que aconteceu (por exemplo, `person.created`) |
| `data` | O registro completo que foi criado/atualizado/excluído |
| `timestamp` | Quando o evento ocorreu (UTC) |
<Note>
Responda com um **status HTTP 2xx** (200-299) para confirmar o recebimento. Respostas não 2xx são registradas como falhas de entrega.
</Note>
## Validação de Webhook
O Twenty assina cada solicitação de webhook por segurança. Valide as assinaturas para garantir que as solicitações sejam autênticas.
### Cabeçalhos
| Cabeçalho | Descrição |
| ---------------------------- | ------------------------ |
| `X-Twenty-Webhook-Signature` | Assinatura HMAC SHA256 |
| `X-Twenty-Webhook-Timestamp` | Timestamp da solicitação |
### Etapas de validação
1. Obtenha o timestamp de `X-Twenty-Webhook-Timestamp`
2. Crie a string: `{timestamp}:{JSON payload}`
3. Calcule o HMAC SHA256 usando o segredo do seu webhook
4. Compare com `X-Twenty-Webhook-Signature`
### Exemplo (Node.js)
```javascript
const crypto = require("crypto");
const timestamp = req.headers["x-twenty-webhook-timestamp"];
const payload = JSON.stringify(req.body);
const secret = "your-webhook-secret";
const stringToSign = `${timestamp}:${payload}`;
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(stringToSign)
.digest("hex");
const receivedSignature = req.headers["x-twenty-webhook-signature"];
const isValid = crypto.timingSafeEqual(
Buffer.from(expectedSignature, "hex"),
Buffer.from(receivedSignature, "hex")
);
```
## Webhooks vs Fluxos de trabalho
| Método | Direção | Caso de uso |
| ------------------------------------------- | ------- | -------------------------------------------------------------------------------- |
| **Webhooks** | SAÍDA | Notificar automaticamente sistemas externos sobre qualquer alteração de registro |
| **Fluxo de trabalho + Solicitação HTTP** | SAÍDA | Enviar dados para fora com lógica personalizada (filtros, transformações) |
| **Gatilho de webhook de fluxo de trabalho** | ENTRADA | Receber dados no Twenty a partir de sistemas externos |
Para receber dados externos, consulte [Configurar um gatilho de Webhook](/l/pt/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger).