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,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>