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: Sarcini în fundal
description: Predă munca de durată sau supusă limitării ratei către lucrătorii Twenty, punând în coadă o altă execuție a unei funcții logice, în loc să rulezi totul inline.
icon: layer-group
---
O rulare a unei funcții de logică este limitată de `timeoutSeconds` (maximum 900 de secunde). Orice lucru care nu poate fi finalizat în acel interval — o resincronizare completă, un fan-out per înregistrare, un API de la o terță parte care aplică limitare de rată — trebuie împărțit în execuții mai mici.
`enqueueJobs` face exact asta: le cere lucrătorilor Twenty să ruleze mai târziu una dintre funcțiile de logică ale aplicației tale, o dată pentru fiecare payload, fiecare rulare în propriul său proces, cu propriul său buget de timeout. Apelantul returnează imediat.
```text
┌─────────────────┐ enqueueJobs(...) ┌──────────────┐ ┌────────────────────┐
│ Logic function │ ─────────────────▶ │ Job queue │──▶│ Logic function │
│ (returns now) │ │ (workers) │ │ (fresh run/timeout)│
└─────────────────┘ └──────────────┘ └────────────────────┘
```
## Pune rulările în coadă
Importă `enqueueJobs` din `twenty-sdk/logic-function`, indică-l către `universalIdentifier` al funcției logice de rulat și transmite câte un payload pentru fiecare rulare funcției enqueue.
```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 }],
});
```
Fiecare rulare primește payloadul său ca argument al handlerului, exact ca orice alt declanșator. Ținta trebuie să aparțină **aceleiași aplicații** ca apelantul — punerea în coadă a funcției altei aplicații este respinsă cu `Logic function not found` și nu se pune nimic în coadă. Un singur apel acceptă până la `200` payloaduri.
<Note>
`enqueueJobs` revine imediat ce joburile sunt acceptate, nu când au fost rulate. Nu întoarce rezultatele țintelor — fă ca fiecare țintă să scrie ce produce în [magazinul cheie-valoare](/l/ro/developers/extend/apps/logic/key-value-store) sau într-o înregistrare din spațiul de lucru dacă trebuie să le citești ulterior.
</Note>
<Note>
Ajutorul mai vechi `enqueueJob`, care pune în coadă un singur job per apel, este depreciat. Folosește în schimb `enqueueJobs` cu o listă `payloads` cu un element.
</Note>
## Opțiuni job
Opțiunile se aplică fiecărei rulări din lot.
| Opțiune | Implicit | Interval | Ce face |
| ------------ | -------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `retryLimit` | `0` | `0`–`10` | Numărul total de încercări suplimentare în coadă. Reîncercările solicitate de aplicație sunt limitate la `3`. Crește această valoare numai pentru handleri care sunt siguri de rulat de două ori. |
| `delayMs` | `0` | `0`–`604800000` (7 zile) | Așteaptă atât timp înainte ca rulările să devină eligibile. |
```ts
await enqueueJobs({
logicFunctionUniversalIdentifier: '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33',
payloads: [{ page: 1 }],
retryLimit: 3,
delayMs: 60_000,
});
```
<Note>
**Prioritatea nu este încă configurabilă.** Joburile puse în coadă rulează întotdeauna cu cea mai joasă prioritate, astfel încât activitatea platformei să nu fie niciodată întârziată de joburile aplicației. Controlul asupra priorității va fi disponibil în curând.
</Note>
Rularea pusă în coadă moștenește utilizatorul activ al funcției care a pus-o în coadă, astfel încât acționează cu aceleași permisiuni.
## Reîncercați după un eșec temporar
Twenty nu reîncearcă fiecare excepție din codul aplicației. O eroare obișnuită lansată este tratată ca un eșec permanent. Pentru un eșec temporar, o funcție logică pusă în coadă poate solicita până la trei reîncercări prin lansarea `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}`,
);
}
};
```
Lansați direct `RetryableLogicFunctionError` atunci când este posibil. Dacă o extindeți, nu îi înlocuiți `name`: Twenty recunoaște numele serializat `RetryableLogicFunctionError` în toate mediile de execuție.
`retryCount` este `0` pentru execuția inițială și crește doar atunci când codul aplicației solicită o reîncercare. `maxRetries` este cel mult `3` și poate fi mai mic atunci când sarcina din coadă are o limită totală de reîncercări mai mică. Eșecurile platformei nu cresc `retryCount`, deși consumă în continuare bugetul general de siguranță al cozii.
Coada întârzie încercările de reîncercare folosind backoff exponențial și jitter. Întârzierea exactă nu este garantată în mod intenționat, astfel că codul aplicației nu ar trebui să depindă de producerea unei reîncercări la un moment precis. După ce se atinge `maxRetries`, o altă `RetryableLogicFunctionError` este înregistrată ca eșecul final al aplicației, fără o altă execuție.
<Warning>
Reîncercările rulează din nou întregul handler și se pot produce după ce unele efecte secundare au reușit. Faceți handlerul idempotent înainte de a solicita reîncercări.
</Warning>
## Folosește-l: parcurge pe pagini o sincronizare lungă
Forma clasică este o funcție care pune în coadă *ea însăși* cu următorul cursor. Fiecare rulare procesează o pagină de lucru bine în interiorul propriului timeout, iar lanțul se oprește când nu mai rămâne nimic.
```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,
});
```
## Dispersare pe înregistrare
Când munca este în mod natural pe element, pune în coadă un job per element într-un singur apel și lasă workerii să le proceseze în paralel în loc să iterezi inline.
```ts
const companies = await listCompaniesToEnrich();
await enqueueJobs({
logicFunctionUniversalIdentifier: ENRICH_COMPANY,
payloads: companies.map((company) => ({ companyId: company.id })),
retryLimit: 2,
});
```
## Bune practici pentru muncă de lungă durată
Două reguli acoperă aproape orice job lung: folosește recursie în locul buclelor și procesează un segment limitat per rulare.
O rulare care încearcă să facă totul este modul de eșec — atinge timeout-ul, iar la o reîncercare începe din nou totul de la zero. În schimb, dimensionează un segment astfel încât să se termine confortabil în interiorul `timeoutSeconds`, persistă-ți poziția și pune în coadă următoarea rulare.
```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,
});
```
Ce îl face robust:
* **Dimensionează segmentul pornind de la cel mai lent element, nu de la medie.** `CHUNK_SIZE × worst-case item time` trebuie să încapă în `timeoutSeconds` cu marjă, altfel „coada” unui segment se pierde când rularea este întreruptă.
* **Fă condiția de terminare explicită.** Apelează recursiv doar cât timp a revenit un segment complet. Un lanț care se oprește doar pe baza „niciun rezultat” va continua la nesfârșit dacă sursa returnează vreodată o pagină scurtă la mijloc.
* **Păstrează progresul înainte de a pune în coadă următoarea rulare**, astfel încât o verigă eșuată să repornească de la ultimul segment finalizat în loc de la început.
* **Păstrează fiecare segment idempotent.** Reprocesarea unui segment după o reîncercare nu trebuie să ducă la scrieri duble — leagă scrierile de înregistrarea sau ID-ul extern pe care îl procesezi.
* **Preferă un lanț segmentat în locul unei dispersări uriașe** atunci când munca lovește o terță parte cu limită de rată: un lanț cu `delayMs` își dozează singur ritmul, în timp ce mii de joburi puse în coadă deodată devin toate eligibile imediat.

View File

@@ -0,0 +1,269 @@
---
title: Conexiuni
description: Permite aplicației tale să acționeze în numele unui utilizator în servicii ale terților prin OAuth.
icon: plug
---
Conexiunile sunt acreditări pe care un utilizator le deține pentru un serviciu extern (Linear, GitHub, Slack, ...). Aplicația ta declară **cum** sunt obținute acele acreditări — un **furnizor de conexiune** — și le folosește în timpul execuției pentru a efectua apeluri autentificate către API-ul terț.
În prezent este acceptat doar OAuth 2.0. Tipurile viitoare de acreditări (jetoane de acces personale, chei API, autentificare de bază) se vor integra în aceeași interfață — aplicațiile care deja folosesc `defineConnectionProvider({ type: 'oauth', ... })` nu vor trebui să migreze.
<AccordionGroup>
<Accordion title="defineConnectionProvider" description="Declară cum sunt obținute conexiunile aplicației tale">
Un furnizor de conexiune descrie handshake-ul OAuth de care are nevoie aplicația ta. Utilizatorul face clic pe "Adaugă conexiune" în setările aplicației tale, completează ecranul de consimțământ al furnizorului și este creată o înregistrare `ConnectedAccount` în spațiul său de lucru.
O configurație funcțională are nevoie de **două fișiere** — furnizorul de conexiune și o declarație `serverVariables` corespunzătoare în `defineApplication` care conține acreditările clientului 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,
},
},
});
```
Puncte cheie:
* `name` este șirul identificator unic folosit în `listConnections({ providerName })` (kebab-case, trebuie să corespundă `^[a-z][a-z0-9-]*$`).
* `displayName` apare în fila de setări a aplicației și în lista de instrumente AI.
* `clientIdVariable` / `clientSecretVariable` sunt **nume**, nu valori — trebuie să se potrivească cheilor declarate în `defineApplication.serverVariables`. Valorile reale `client_id` și `client_secret` sunt introduse de administratorul serverului prin interfața de înregistrare a aplicației și nu sunt niciodată comise în repo-ul tău.
* Folosește `serverVariables` (nu `applicationVariables`) — acreditările OAuth sunt la nivel de server și există o singură aplicație OAuth pentru fiecare server Twenty.
* Până când ambele `serverVariables` sunt completate, fila de setări a aplicației afișează un indiciu "necesită administrator de server" și butonul "Adaugă conexiune" este dezactivat.
* `type: 'oauth'` este singura valoare acceptată în prezent. Discriminatorul este compatibil cu versiuni viitoare: tipurile viitoare (`'pat'`, `'api-key'`, ...) vor adăuga blocuri noi de sub-configurație alături de `oauth`.
URL-ul de callback OAuth pe care furnizorul tău trebuie să îl includă pe lista albă este:
```
https://<your-twenty-server>/auth/apps/callback
```
</Accordion>
<Accordion title="Rulează o funcție logică la conectare" description="Reacționează în momentul în care o conexiune este stabilită">
Unii furnizori îți furnizează date în momentul conectării, pe care trebuie să le stochezi înainte ca conexiunea să poată fi utilizată — exemplul clasic este Slack, unde răspunsul OAuth identifică `team_id` al spațiului de lucru după care vor fi indexate evenimentele primite. Setează `onConnectLogicFunction` pentru a face referire la o funcție logică din aceeași aplicație (prin `universalIdentifier`), iar aceasta rulează imediat după ce `ConnectedAccount` este creat.
```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',
},
});
```
Hook-ul rulează **asincron în spațiul de lucru care se conectează** (este pus în coadă, nu este așteptat), astfel încât un hook lent sau care eșuează nu blochează și nu întrerupe niciodată callback-ul OAuth — fă-l idempotent și lasă-l să își gestioneze singur reîncercările. Handlerul primește:
```ts
type OnConnectPayload = {
connectionProviderId: string;
connectionProviderName: string; // e.g. 'slack'
connectedAccountId: string;
};
```
De acolo folosește `getConnection(connectedAccountId)` pentru a citi tokenul de acces proaspăt și a apela API-ul furnizorului (de ex. Slack `auth.test`) sau pentru a stoca o mapare în [magazinul cheie–valoare](/l/ro/developers/extend/apps/logic/key-value-store).
</Accordion>
<Accordion title="Rulează o funcție logică la deconectare" description="Curăță resursele atunci când o conexiune este eliminată">
Orice revendică o aplicație la momentul conectării trebuie eliberat atunci când conexiunea este eliminată. O integrare Slack care revendică un `team_id` la conectare, de exemplu, trebuie să elibereze acea revendicare astfel încât un alt spațiu de lucru să poată conecta aceeași echipă Slack. Setează `onDisconnectLogicFunction` pentru a face referire la o funcție logică din aceeași aplicație, iar aceasta rulează imediat după ce `ConnectedAccount` este șters.
```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',
},
});
```
La fel ca hook-ul on-connect, acesta rulează **asincron în spațiul de lucru care se deconectează** și nu blochează niciodată deconectarea. Handler-ul primește aceeași structură de payload:
```ts
type OnDisconnectPayload = {
connectionProviderId: string;
connectionProviderName: string; // e.g. 'slack'
connectedAccountId: string;
};
```
`ConnectedAccount` nu mai există atunci când rulează hook-ul, astfel încât `getConnection(connectedAccountId)` nu se mai rezolvă. Tot ceea ce îi este necesar procesului de curățare (un `team_id`, un ID extern de abonament) trebuie să fi fost scris în [key-value store](/l/ro/developers/extend/apps/logic/key-value-store) la momentul conectării, indexat după `connectedAccountId`.
Hook-ul este declanșat atunci când o conexiune este eliminată în mod direct. Dezinstalarea aplicației elimină conexiunile acesteia printr-un mecanism în cascadă în baza de date, astfel încât hook-ul nu rulează în acest caz. Declară un `uninstallLogicFunction` pe `defineApplication` pentru acel flux: acesta rulează înainte ca metadatele aplicației să fie șterse, astfel încât poate apela în continuare `listConnections` și poate curăța tot ce a mai rămas.
</Accordion>
<Accordion title="listConnections / getConnection" description="Folosește conexiunile dintr-o funcție logică">
În interiorul unui handler de funcție logică, `listConnections({ providerName })` returnează înregistrările `ConnectedAccount` ale acestei aplicații pentru furnizorul dat, cu tokenuri de acces reîmprospătate.
```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 };
};
```
Fiecare conexiune are:
| Câmp | Descriere |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | ID unic al înregistrării; pasează-l la `getConnection(id)` pentru a reobține acea înregistrare |
| `visibility` | `'user'` (privată pentru un membru al spațiului de lucru) sau `'workspace'` (partajată cu toți membrii) |
| `scopes` | Permisiunile OAuth acordate de furnizorul upstream (distincte de `visibility` — nu au legătură) |
| `userWorkspaceId` | ID-ul userWorkspace al deținătorului — util pentru a alege "conexiunea utilizatorului care face cererea" în declanșatoarele de rută HTTP |
| `accessToken` | Token de acces OAuth proaspăt (reîmprospătat automat dacă a expirat) |
| `name` | Numele afișat al conexiunii (derivat automat la callback-ul OAuth, poate fi redenumit de utilizator) |
| `handle` | Emailul contului din amonte conectat, actualizat la fiecare reconectare; rezolvat din `id_token` OIDC a revenit la schimbul tokenului, revenind la e-mailul de conectare a douăzeci de utilizatori atunci când furnizorul nu returnează unul |
| `authFailedAt` | Setat când cea mai recentă reîmprospătare a eșuat; utilizatorul trebuie să se reconecteze |
Puncte cheie:
* Pasează `{ providerName }` pentru a filtra după furnizor; omite-l pentru a obține toate conexiunile pe care această aplicație le deține la toți furnizorii.
* Serverul reîmprospătează transparent tokenul de acces înainte de a returna. Handlerul tău vede întotdeauna un token utilizabil (sau `authFailedAt` setat).
* `getConnection(id)` este echivalentul pentru o singură înregistrare.
</Accordion>
<Accordion title="Vizibilitate per utilizator vs partajată la nivel de spațiu de lucru" description="Cum aleg utilizatorii între acreditări private și partajate">
Când un utilizator face clic pe "Adaugă conexiune", i se solicită să aleagă o vizibilitate:
* **Doar pentru mine** — acreditarea este privată pentru utilizatorul care se conectează. Orice funcție logică apelată în numele lor (declanșator de rută HTTP cu `isAuthRequired: true`) o vede; declanșatoarele cron și evenimentele din bază de date nu.
* **Partajată la nivel de spațiu de lucru** — orice membru al spațiului de lucru poate folosi acreditarea. Declanșatoarele cron / din bază de date o văd, de asemenea, deoarece nu au un utilizator al cererii.
Folosește-o pe cea potrivită pentru fiecare 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');
```
Sunt permise mai multe conexiuni per (utilizator, furnizor), astfel încât același utilizator poate avea "Personal Linear" și "Work Linear" una lângă alta.
</Accordion>
<Accordion title="Configurare unică a furnizorului" description="Înregistrează-ți aplicația OAuth la serviciul terț">
Pentru fiecare furnizor de conexiune, administratorul serverului trebuie mai întâi să înregistreze o aplicație OAuth la serviciul terț.
1. Mergi la setările pentru dezvoltatori ale furnizorului (de ex. https://linear.app/settings/api/applications/new).
2. Setează **Redirect URI** la `\<SERVER_URL>/auth/apps/callback`.
3. Copiază **Client ID** și **Client Secret** generate.
4. Deschide aplicația instalată în Twenty ca administrator de server → setează valorile pe `serverVariables` corespunzătoare.
5. Membrii spațiului de lucru pot apoi să adauge conexiuni din secțiunea **Conexiuni** a fiecărei aplicații.
</Accordion>
</AccordionGroup>

View File

@@ -0,0 +1,110 @@
---
title: Stocare cheie-valoare
description: Păstrați rezultatele intermediare, puneți datele în cache și partajați starea între rulările funcțiilor logice cu spațiul de stocare cheie-valoare integrat al aplicației.
icon: database
---
Funcțiile logice rulează în sandbox în procese Node.js de scurtă durată — odată ce o rulare se termină, nimic din ce a fost păstrat în memorie nu supraviețuiește. Când ai nevoie să **îți amintești ceva între rulări** (să pui în cache un răspuns API costisitor, să stochezi un cursor pentru sincronizări incrementale, să aplici debounce sau să transmiți starea de la o funcție la alta), salvează-l în spațiul de stocare cheie-valoare integrat.
Fiecare aplicație primește propriul spațiu de nume izolat: intrările sunt asociate cu aplicația autentificată, astfel încât cheile tale nu pot intra niciodată în coliziune cu — sau fi citite de — o altă aplicație.
```text
┌─────────────────┐ kv.set(key, value) ┌──────────────────────────┐
│ Logic function │ ─────────────────────▶ │ Application KV store │
│ (your handler) │ ◀───────────────────── │ key (unique) │ value │
└─────────────────┘ kv.get(key) └──────────────────────────┘
```
## Get, set, delete
Importați `kv` din `twenty-sdk/logic-function`. Valorile pot fi orice payload serializabil 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');
```
## Domenii de aplicare
Fiecare intrare are un domeniu de aplicare, transmis ca opțiune la fiecare apel. Valoarea implicită este `WORKSPACE`.
* **`WORKSPACE`** (implicit) — intrarea este privată pentru instalarea curentă a workspace-ului aplicației tale. Fiecare workspace care instalează aplicația primește propriul set independent de chei. Aceasta este opțiunea potrivită pentru cache-uri, cursoare și stare specifică fiecărui workspace.
* **`SERVER`** — intrarea este partajată între **fiecare instalare** a aplicației tale de pe server. Intrările de tip server se comportă ca niște **claim-uri**: valoarea stocată este întotdeauna workspaceId-ul care a revendicat cheia (omite `value` la `set` pentru a revendica cheia pentru workspace-ul curent), iar doar acel workspace o poate suprascrie sau șterge. Orice instalare poate citi intrarea.
Revendicările de server există pentru rutare între workspace-uri. Un [server-route resolver](/l/ro/developers/extend/apps/logic/logic-functions#server-route-trigger) rulează în workspace-ul proprietar al înregistrării aplicației, dar un webhook de intrare, de obicei, conține doar un id de cont extern — nu un Twenty workspaceId. Configurați fiecare workspace să își revendice ID-ul extern la momentul conectării, apoi rezolvați-l în rută:
```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',
});
```
Deoarece o cheie de tip server poate fi revendicată doar pentru propriul workspace al apelantului și nu poate fi niciodată suprascrisă de un altul, un workspace nu poate deturna o mapare care aparține altcuiva. `kv.set` aruncă o eroare atunci când cheia este deja revendicată de un alt workspace.
## Folosește-l: păstrează în cache un apel costisitor
O utilizare tipică este păstrarea în cache a unui răspuns lent sau cu limitare de rată de la un terț, astfel încât rulările repetate să îl refolosească în loc să plătească costul de fiecare dată.
```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,
});
```
## Tipare și sfaturi
* **Spații de nume.** Prefixează cheile pentru a păstra domeniile diferite separate — `sync-cursor:linear`, `cache:exchange-rate:USD:EUR`, `lock:nightly-report`.
* **Expiry (TTL).** Store-ul nu are expirare integrată. Stochează un timestamp în interiorul valorii (ca în exemplul de cache) și verifică-l la citire sau curăță cheile învechite dintr-o [funcție declanșată de cron](/l/ro/developers/extend/apps/logic/logic-functions).
* **Ce să stochezi.** Orice valoare serializabilă JSON — numere, stringuri, array-uri, obiecte. Păstrează intrările mici; acesta este pentru coordonare și caching, nu pentru blob-uri mari sau fișiere. Pentru fișiere, folosește un câmp `FILES` și [`uploadFile`](/l/ro/developers/extend/apps/logic/logic-functions#uploading-files).
* **Vizibilitate.** Intrările există în baza de date a instanței, nu ca înregistrări ale workspace-ului — nu apar niciodată în interfața workspace-ului, nu fac parte din modelul de date al aplicației tale și nu necesită permisiuni de rol sau de obiect.
## Alternativă: un obiect de stocare interogabil
Spațiul de stocare integrat este, în mod deliberat, opac: intrările nu sunt înregistrări, astfel că nu le poți răsfoi în UI, nu le poți corela cu alte obiecte și nu le poți filtra cu interogări de înregistrări. Când ai nevoie de oricare dintre acestea — de exemplu un jurnal de sincronizare vizibil sau o stare per-înregistrare — definește în schimb un mic **obiect tehnic** cu un câmp `key` unic și un câmp `RAW_JSON` `value`, și interoghează-l prin [clientul API tipizat](/l/ro/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk). Vezi [Objects](/l/ro/developers/extend/apps/data/objects) pentru referința `defineObject` și [Data → Unique indexes](/l/ro/developers/extend/apps/data/overview#unique-indexes) pentru aplicarea unicității cheilor.
* **Limitarea la o înregistrare.** Adaugă o [relație](/l/ro/developers/extend/apps/data/relations) de la obiectul de stocare la obiectul țintă, în loc să codifici id-ul în cheie.
* **Vizibilitate și permisiuni.** Rândurile trăiesc în baza de date a workspace-ului ca orice altă înregistrare, astfel încât pot fi interogate prin API și respectă [rolul](/l/ro/developers/extend/apps/config/roles) aplicației tale. Pentru a ține store-ul în afara UI-ului principal, nu îl include în [navigation menu](/l/ro/developers/extend/apps/layout/navigation-menu-items).
<Note>
Spre deosebire de spațiul de stocare integrat, un obiect personalizat este întotdeauna limitat la un singur workspace — nu poate partaja intrări între instalări, așa cum fac cheile `SERVER`.
</Note>

View File

@@ -0,0 +1,909 @@
---
title: Funcții logice
description: Definește funcții TypeScript pe partea de server cu declanșatoare HTTP, cron și de evenimente din baza de date.
icon: bolt
---
Funcțiile de logică sunt funcții TypeScript pe partea de server care rulează pe platforma Twenty. Acestea pot fi declanșate de solicitări HTTP, programări cron sau evenimente din baza de date — și pot fi, de asemenea, expuse ca instrumente pentru agenți AI.
<AccordionGroup>
<Accordion title="defineLogicFunction" description="Definiți funcții logice și declanșatoarele acestora">
Fiecare fișier de funcție folosește `defineLogicFunction()` pentru a exporta o configurație cu un handler și declanșatoare opționale.
```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 *',
},*/
});
```
Tipuri de declanșatoare disponibile:
* **httpRoute**: Expune funcția pe o cale și o metodă HTTP. În codul aplicației, prefixează calea rutei cu `/s/` când folosești `RestApiClient`; URL-ul implementat folosește baza injectată `TWENTY_FUNCTIONS_URL` (sau `\<server-url>/s` atunci când nu este setată).
<Note>
Pentru a apela o funcție logică declanșată de o rută dintr-o componentă front-end (headless), consultă [Apelarea unei funcții logice](/l/ro/developers/extend/apps/layout/front-components#calling-a-logic-function).
</Note>
* **cron**: Rulează funcția pe un program folosind o expresie CRON.
* **databaseEvent**: Rulează la evenimentele ciclului de viață ale obiectelor din spațiul de lucru. Când operațiunea evenimentului este `updated`, câmpurile specifice de urmărit pot fi specificate în array-ul `updatedFields`. Dacă este lăsat nedefinit sau gol, orice actualizare va declanșa funcția.
> de ex. `person.updated`, `*.created`, `company.*`
* **serverRoute**: Expune o singură rută HTTP la nivelul înregistrării. O funcție de tip **resolver** (declarată cu `serverRouteTriggerSettings`) rulează în workspace-ul proprietar și fie returnează un `Response` sincron, fie workspace-ul țintă ȘI funcția logică de pus în coadă; pe ramura de punere în coadă, platforma confirmă cu `202` și rulează acea **țintă** în coada worker-ului. Consultați [declanșatorul de rută de server](#server-route-trigger).
<Note>
Puteți, de asemenea, să executați manual o funcție folosind 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
```
Puteți urmări jurnalele cu:
```bash filename="Terminal"
yarn twenty dev:function:logs
```
</Note>
#### Payload-ul declanșatorului de rută
Când un declanșator de rută invocă funcția logică, aceasta primește un obiect `RoutePayload` care urmează
[AWS HTTP API v2 format](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html).
Importați tipul `RoutePayload` din `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' };
};
```
Tipul `RoutePayload` are următoarea structură:
| Proprietate | Tip | Descriere | Exemplu |
| ---------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `headers` | `Record\<string, string \| undefined>` | Anteturi HTTP (doar cele listate în `forwardedRequestHeaders`) | consultați secțiunea de mai jos |
| `queryStringParameters` | `Record\<string, string \| undefined>` | Parametri query string (valorile multiple unite cu virgule) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
| `pathParameters` | `Record\<string, string \| undefined>` | Parametri de cale extrași din modelul rutei | `/users/:id`, `/users/123` -> `{ id: '123' }` |
| `body` | `object \| null` | Corpul cererii analizat (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
| `rawBody` | `string \| undefined` | Corpul original al cererii în UTF-8, înainte de parsarea JSON. Util pentru verificarea semnăturilor de tip HMAC pentru webhook-uri (de exemplu, `X-Hub-Signature-256` de la GitHub, Stripe). `undefined` atunci când mediul de execuție nu a păstrat-o. | |
| `isBase64Encoded` | `boolean` | Indică dacă corpul este codificat în base64 | |
| `requestContext.http.method` | `string` | Metoda HTTP (GET, POST, PUT, PATCH, DELETE) | |
| `requestContext.http.path` | `string` | Calea brută a cererii | |
#### forwardedRequestHeaders
În mod implicit, anteturile HTTP din cererile de intrare **nu** sunt transmise funcției dvs. de logică din motive de securitate.
Pentru a accesa anumite anteturi, listați-le explicit în array-ul `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'],
},
});
```
În handler, accesați anteturile transmise mai departe astfel:
```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>
Numele anteturilor sunt normalizate la litere mici. Accesați-le folosind chei cu litere mici (de exemplu, `event.headers['content-type']`).
</Note>
#### Răspuns HTTP personalizat
În mod implicit, returnarea unei valori simple din handler trimite înapoi un răspuns `200` (JSON pentru obiecte, `text/plain` pentru șiruri). Pentru a controla codul de stare și antetele răspunsului, returnează un `Response` din `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' },
});
};
```
Din motive de securitate, anteturile de răspuns sunt limitate la o listă de antete permise. Orice antet care nu se află pe listă (de exemplu, `Set-Cookie`, anteturi CORS precum `Access-Control-Allow-Origin` sau anteturi personalizate `X-*`) este eliminat în mod silențios înainte ca răspunsul să fie trimis. Anteturile de răspuns permise sunt:
* `content-type`
* `content-language`
* `content-disposition`
* `cache-control`
* `retry-after`
<Note>
Codul de stare trebuie să fie un cod de stare HTTP valid (între 100 și 599). Numele anteturilor de răspuns sunt comparate fără a ține cont de majuscule și minuscule.
</Note>
#### Răspunsuri de eroare ale platformei
Pe lângă propriile răspunsuri ale handlerului tău, platforma răspunde direct la apelurile către rute în anumite situații: `404` când ruta sau funcția nu există, `403` când aplicația este oprită, `429` când este atinsă limita ratei de execuție și `422` când `dependencies` de producție ale aplicației sunt prea mari pentru a fi instalate — vezi [limite privind dimensiunea dependențelor](/l/ro/developers/extend/apps/getting-started/project-structure#dependencies).
#### Declanșator de rută de server
`httpRouteTriggerSettings` expune o funcție sub `/s/` și rezolvă spațiul de lucru din gazda cererii — ceea ce funcționează atunci când fiecare spațiu de lucru are propriul domeniu. Furnizorii terți, însă, livrează evenimentele fiecărui tenant către **un** singur URL. Pentru acest caz, folosiți `serverRouteTriggerSettings`.
Declanșatorul are două părți:
1. O funcție logică de **resolver** — declarată cu `serverRouteTriggerSettings` — rulează în **workspace-ul deținător** (workspace-ul care deține înregistrarea aplicației). Inspectează cererea primită și returnează fie:
* `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }` — platforma pune în coadă acea țintă în workspace-ul rezolvat și confirmă cu `202 { queued: true }`, sau
* un `Response` de la `twenty-sdk/logic-function` — platforma transmite mai departe acel răspuns HTTP **sincron** și **nu** pune în coadă nicio țintă (folosiți acest lucru pentru handshake-uri de tip challenge, cum ar fi Slack `url_verification`).
Resolver-ul este singurul punct de autorizare — URL-ul conține doar identificatorul resolver-ului. **Acesta este locul preferat pentru a verifica semnăturile cererilor**: resolver-ul rulează înaintea oricărui efect secundar, are acces la `rawBody` original și la headerele redirecționate și poate respinge fără a atinge vreodată ținta.
2. O funcție logică **țintă** — o funcție logică obișnuită per-workspace — rulează apoi în workspace-ul rezolvat cu payload-ul returnat de resolver (sau payload-ul original al cererii dacă resolver-ul nu l-a transformat). Valoarea de returnare **nu** este observată de apelantul HTTP atunci când resolver-ul a ales calea de punere în coadă.
```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,
});
```
Endpoint-ul este accesibil la:
```
POST https://your-twenty-server.com/webhooks/server/:resolverLogicFunctionUniversalIdentifier
```
Identificatorul este `universalIdentifier` al resolver-ului din manifestul dvs. Înregistrați acel URL la furnizor.
**Răspunsul la un GET de verificare.** Unii furnizori verifică un punct final înainte de a livra către acesta, trimițând un `GET` care conține o provocare la același URL către care vor trimite ulterior evenimente prin `POST` — WhatsApp Cloud API de la Meta este unul dintre aceștia. O rută de server răspunde numai la `POST`, cu excepția cazului în care specifici altfel, așa că declară ambele metode:
```ts
serverRouteTriggerSettings: {
forwardedRequestHeaders: ['x-hub-signature-256'],
httpMethods: ['GET', 'POST'],
}
```
Provocarea ajunge în `event.queryStringParameters`, iar returnarea unui `Response` o retransmite furnizorului în aceeași cerere. Un corp de tip șir este trimis ca `text/plain`, ceea ce așteaptă acești furnizori:
```ts
const handler = async (event: RoutePayload) => {
if (event.requestContext.http.method === 'GET') {
return new Response(event.queryStringParameters['hub.challenge']);
}
// …handle the delivered event
};
```
`httpMethods` înlocuiește valoarea implicită, în loc să se adauge la aceasta, astfel încât doar `['GET']` face ca ruta să respingă `POST`. Sunt acceptate numai `GET` și `POST`. Lăsați-o nesetată, cu excepția cazului în care furnizorul are nevoie de al doilea verb: o rută care declară `GET` își va executa resolverul pentru orice apelant neautentificat, inclusiv pentru crawlerele și serviciile de desfășurare a linkurilor care trimit `GET` fără solicitare. Orice pentru care platforma nu are o metodă primește răspunsul `405`, fără ca resolverul să fie vreodată executat.
<Note>
**Aplicația trebuie revendicată și instalată în spațiul de lucru al proprietarului.** Deoarece resolverul rulează în **spațiul de lucru al proprietarului** (spațiul de lucru care deține înregistrarea aplicației), un declanșator de rută de server funcționează doar după ce aplicația a fost *revendicată* — adică are un spațiu de lucru al proprietarului — **și** acea aplicație este **instalată în spațiul de lucru al proprietarului**. Până când ambele condiții sunt adevărate, resolverul nu are unde să ruleze, astfel ruta nu poate fi apelată. O aplicație care expune o funcție logică `serverRouteTriggerSettings` nu poate fi, așadar, listată în marketplace până când nu este revendicată și instalată în spațiul de lucru al proprietarului.
</Note>
**Contractul resolver-ului.** Tipul `LogicFunctionConfig` din SDK impune acest lucru la compilare: de îndată ce setați `serverRouteTriggerSettings`, handler-ul este constrâns să returneze fie un `Response`, fie `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }` (sau un `Promise` al uneia dintre acestea). Pe calea de trimitere, `workspaceId` trebuie să fie un workspace în care funcția țintă este instalată, altfel cererea este respinsă cu `404`. Un rezultat care nu corespunde niciuneia dintre forme — inclusiv unul ale cărui identificatoare nu sunt UUID-uri — este respins cu `502`.
| Câmp | Tip | Notițe |
| ---------------------------------------- | ------------------- | --------------------------------------------------------------------------------- |
| `workspaceId` | `șir` | UUID-ul workspace-ului în care va rula ținta. |
| `targetLogicFunctionUniversalIdentifier` | `string` | `universalIdentifier` al funcției logice care trebuie invocată în acel workspace. |
| `payload` | `object` (opțional) | Dacă este setat, înlocuiește corpul cererii trimis către țintă. |
<Warning>
**Verificarea semnăturii este responsabilitatea dvs. — verificați în resolver.** Platforma nu verifică semnăturile cererilor. Resolver-ul este locul recomandat pentru a face acest lucru: rulează primul, cu acces la `event.rawBody` și la headerele pe care le-ați enumerat în `forwardedRequestHeaders`, iar o eroare aruncată (sau orice `workspaceId` care nu se potrivește) oprește livrarea înainte ca ținta să fie invocată. Dacă, în schimb, mutați verificarea în funcția țintă, funcția țintă trebuie să aibă grijă să nu piardă `rawBody` și headerele — adică resolver-ul nu trebuie să returneze un `payload`. Verificați întotdeauna **înainte** de orice efect secundar și folosiți o comparație în timp constant.
</Warning>
Pentru semnăturile cererilor, majoritatea furnizorilor semnează cu HMAC-SHA256; părțile care diferă sunt numele headerului, codificarea digestului și șirul de payload semnat. Câteva exemple:
| Furnizor | Headere de redirecționat | Șir semnat | Digest |
| ---------------------------- | ------------------------------------------------------ | ---------------------------- | -------------------------------------------------------------------- |
| Svix (Recall, Resend, Clerk) | `webhook-id`, `webhook-timestamp`, `webhook-signature` | `{id}.{timestamp}.{rawBody}` | base64 (secretul este în base64 după eliminarea prefixului `whsec_`) |
| Stripe | `stripe-signature` | `{timestamp}.{rawBody}` | hex |
| GitHub | `x-hub-signature-256` | `{rawBody}` | hex (cu prefixul `sha256=`) |
| Shopify | `x-shopify-hmac-sha256` | `{rawBody}` | base64 |
| Slack | `x-slack-signature`, `x-slack-request-timestamp` | `v0:{timestamp}:{rawBody}` | hex (cu prefixul `v0=`) |
Exemplul de resolver de mai sus arată deja fluxul GitHub HMAC-SHA256 — adaptați numele headerului, codificarea digestului și șirul de payload semnat în funcție de furnizorul cu care vă integrați.
<Note>
Când resolver-ul returnează un obiect de dispatch, ruta răspunde cu `202 { queued: true }`, iar ținta rulează în coada worker-ului — apelantul nu observă niciodată latența, rezultatul sau erorile țintei (acestea sunt înregistrate în jurnalele de execuție). Acest lucru împiedică retrimiterile expeditorului să amplifice încetinirile procesării, ceea ce este de dorit pentru ingestia de webhook-uri.
Atunci când apelantul trebuie să citească corpul răspunsului în cadrul aceleiași cereri (challenge handshakes, confirmări interactive), returnați în schimb un `Response` din **resolver**. Platforma îl reflectă sincron și omite coada; header-ele acestuia trec prin aceeași listă de permisiuni ca răspunsurile rutelor HTTP. Mențineți resolver-ul rapid — unii furnizori (de ex. Slack) expiră după câteva secunde. Deoarece resolver-ul este accesibil ca endpoint public, protejați-l cu limitare de rată la marginea infrastructurii dvs.
</Note>
#### Payload-ul declanșatorului de eveniment al bazei de date
Când un declanșator de eveniment al bazei de date apelează funcția dvs. logică, aceasta primește un `DatabaseEventPayload` pentru fiecare înregistrare modificată. Payload-ul combină metadatele despre spațiul de lucru și obiectul sursă cu evenimentul la nivel de înregistrare.
```ts
import type {
DatabaseEventPayload,
ObjectRecordCreateEvent,
ObjectRecordDestroyEvent,
ObjectRecordUpdateEvent,
} from 'twenty-sdk/logic-function';
type Person = {
id: string;
emails?: { primaryEmail?: string };
};
```
Payload-ul include:
| Proprietate | Descriere |
| ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
| `name` | Numele evenimentului, cum ar fi `person.updated`. |
| `workspaceId` | Spațiul de lucru în care a avut loc evenimentul. |
| `objectMetadata` | Metadate pentru obiectul care s-a modificat. |
| `recordId` | ID-ul înregistrării modificate. |
| `userId`, `userWorkspaceId`, `workspaceMemberId` | Câmpurile actorului atunci când evenimentul a fost cauzat de un utilizator al spațiului de lucru. |
| `properties` | Datele înregistrării pentru eveniment, cu `before`, `after`, `diff` și `updatedFields` în funcție de operație. |
| Eveniment | Datele înregistrării |
| ------------------ | -------------------------------------------------------------------------------------------------------------- |
| `person.created` | `event.properties.after` |
| `person.updated` | `event.properties.before`, `event.properties.after`, `event.properties.diff`, `event.properties.updatedFields` |
| `person.destroyed` | `event.properties.before` |
Pentru ștergeri logice (soft delete), `.deleted` urmează structura de tip update deoarece câmpul `deletedAt` al înregistrării se modifică.
Pentru ștergeri permanente, folosiți `.destroyed`.
<Note>
`databaseEventTriggerSettings.updatedFields` filtrează ce evenimente de actualizare declanșează funcția.
`event.properties.updatedFields` vă indică ce câmpuri s-au modificat efectiv în evenimentul curent.
</Note>
Exemplu de eveniment de creare:
```ts
type PersonCreatedEvent = DatabaseEventPayload<
ObjectRecordCreateEvent<Person>
>;
const handler = async (event: PersonCreatedEvent) => {
const person = event.properties.after;
return {
personId: event.recordId,
email: person.emails?.primaryEmail,
};
};
```
Exemplu de eveniment de actualizare:
```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,
};
};
```
Declanșare doar la actualizări ale e-mailului:
```ts
export default defineLogicFunction({
...,
databaseEventTriggerSettings: {
eventName: 'person.updated',
updatedFields: ['emails'],
},
});
```
Exemplu de eveniment de ștergere:
```ts
type PersonDestroyedEvent = DatabaseEventPayload<
ObjectRecordDestroyEvent<Person>
>;
const handler = async (event: PersonDestroyedEvent) => {
const personBeforeDestroy = event.properties.before;
return {
personId: event.recordId,
email: personBeforeDestroy.emails?.primaryEmail,
};
};
```
#### Contextul execuției.
Fiecare gestionar primește un al doilea argument care descrie execuția propriu-zisă, indiferent de ce a declanșat-o. În timp ce primul argument își schimbă forma pentru fiecare declanșator, acesta nu se schimbă:
```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 };
};
```
| Câmp | Tip | Descriere |
| ------------------- | ---------------- | ------------------------------------------------------------------------------------------------------ |
| `workspaceId` | `string` | Spațiul de lucru căruia îi aparține execuția. |
| `userWorkspaceId` | `string \| null` | Persoana care a declanșat execuția, ca membru al acestui spațiu de lucru. |
| `workspaceMemberId` | `string \| null` | Aceeași persoană ca înregistrare de membru al spațiului de lucru, la care conectați înregistrări. |
| `retryCount` | `number` | `0` la prima încercare. Consultați [sarcini de fundal](/l/ro/developers/extend/apps/logic/background-jobs). |
| `maxRetries` | `number` | Limita superioară a reîncercărilor solicitate de aplicație. |
`userWorkspaceId` și `workspaceMemberId` sunt `null` atunci când nimeni nu a declanșat execuția: programările cron, hookurile de instalare și webhookurile neautentificate nu au nicio persoană în spatele lor. De asemenea, sunt `null` atunci când persoana nu are o înregistrare de membru al spațiului de lucru sau când aceasta a fost ștearsă.
<Note>
**Contextul vă indică cine a declanșat execuția.** Ceea ce poate *face* execuția este o declarație separată, mai jos.
</Note>
#### Ale cui accesări le utilizează un apel
Fiecare client — `CoreApiClient`, `MetadataApiClient` și `RestApiClient` — acționează ca persoana care a declanșat execuția: rolul acesteia se intersectează cu cel al aplicației dvs., astfel încât apelul nu poate face niciodată mai mult decât poate face oricare dintre voi. Acesta este comportamentul implicit și înseamnă că o persoană nu poate folosi niciodată aplicația dvs. pentru a-și depăși propriile permisiuni.
Când nimeni nu a declanșat execuția, nu există nicio persoană în numele căreia să se acționeze, astfel încât același client revine la accesul propriu al aplicației dvs.: programările cron, hookurile de instalare și webhookurile neautentificate nu necesită o gestionare specială.
Unele apeluri au nevoie în mod legitim de accesul propriu al aplicației chiar și atunci când o persoană *se află* în spatele execuției — de exemplu, pentru a citi înregistrările de configurare ale aplicației dvs. sau pentru a efectua lucrări pe care persoana respectivă nu le-ar putea face singură. Creați un al doilea client pentru acestea:
```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: { ... } });
};
```
Creați-le pe amândouă o singură dată, la nivel de modul, iar fiecare loc de apel indică apoi accesul utilizat prin clientul pe care îl apelează. `RestApiClient` acceptă aceeași opțiune:
```ts
import { RestApiClient } from 'twenty-client-sdk/rest';
const asApplication = new RestApiClient({ runAs: 'application' });
```
<Note>
**O execuție pe care nu a declanșat-o nimeni acționează ca aplicația dvs.** Programările cron, hookurile de instalare și webhookurile neautentificate nu au nicio persoană în spatele lor, astfel încât clientul implicit revine la accesul propriu al aplicației dvs. și continuă să funcționeze. `runAs: 'application'` este necesar doar când doriți acel acces într-o execuție pe care o persoană *a* declanșat-o.
Verificați `context.workspaceMemberId` atunci când o funcție se comportă diferit în funcție de existența unei persoane în spatele ei, de exemplu pentru a atribui o înregistrare.
</Note>
Ajutoarele SDK care accesează resursele proprii ale aplicației dvs. folosesc întotdeauna accesul acesteia și ignoră `runAs`: [stocarea cheie-valoare](/l/ro/developers/extend/apps/logic/key-value-store), [conexiunile](/l/ro/developers/extend/apps/logic/connections), `runAgent`, `getPublicAssetUrl` și taxarea creditelor.
#### Expunerea unei funcții ca instrument AI sau ca acțiune în fluxul de lucru
Funcțiile logice pot fi expuse în două locuri, fiecare cu propriul declanșator:
* **`toolTriggerSettings`** — face funcția descoperibilă de către funcționalitățile AI ale Twenty (chat, MCP, apelarea de funcții). Folosește JSON Schema standard, formatul pe care LLM-urile îl înțeleg nativ.
* **`workflowActionTriggerSettings`** — determină ca funcția să apară ca un pas în constructorul vizual de fluxuri de lucru. Folosește `InputSchema` bogat al Twenty, astfel încât constructorul să poată afișa editori de câmp adecvați, selectoare de variabile și etichete.
O funcție poate opta pentru una, cealaltă sau ambele. Acestea stau alături de `cronTriggerSettings`, `databaseEventTriggerSettings` și `httpRouteTriggerSettings` — același tipar, aceeași formă.
<Note>
**Relația cu acțiunea Code din fluxul de lucru.** Acțiunea integrată **Code** din constructorul de fluxuri de lucru este ea însăși o funcție logică — Twenty creează câte una pentru fiecare pas Code și afișează editorul inline. `workflowActionTriggerSettings` este modul în care transformi acel cod inline, de unică folosință, într-o acțiune **reutilizabilă**: definești funcția o singură dată în aplicația ta și devine selectabilă în orice flux de lucru, în loc să fie copiată și lipită în fiecare pas Code. Vezi [acțiunea Code](/l/ro/user-guide/workflows/capabilities/workflow-actions#code) în ghidul utilizatorului pentru vizualizarea din perspectiva utilizatorului 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: {},
});
```
Puncte cheie:
* O funcție poate combina suprafețele — declară atât `toolTriggerSettings`, cât și `workflowActionTriggerSettings` pentru a o expune atât în chat, cât și în constructorul de fluxuri de lucru.
* `toolTriggerSettings.inputSchema` și `workflowActionTriggerSettings.inputSchema` sunt ambele opționale. Când sunt omise, generatorul de manifest le deduce din codul sursă al handlerului (JSON Schema pentru instrumentul AI, `InputSchema` al Twenty pentru acțiunea de flux de lucru). Furnizează unul în mod explicit atunci când dorești o tipizare mai bogată — de exemplu, cu câmpuri compatibile cu `FieldMetadataType`, precum `CURRENCY` sau `RELATION` pentru constructorul de fluxuri de lucru, sau cu câmpuri `description` pe care agentul AI le poate citi:
```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'],
},
},
});
```
Pentru a declara parametrii **o singură dată** și a deservi ambele suprafețe, definește o singură schemă JSON (`InputJsonSchema`) și convertește-o pentru acțiunea din fluxul de lucru cu `jsonSchemaToInputSchema` din `twenty-sdk/logic-function`. `toolTriggerSettings.inputSchema` primește direct schema JSON, în timp ce `workflowActionTriggerSettings.inputSchema` necesită `InputSchema` al 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),
},
});
```
##### Un exemplu complet de acțiune de flux de lucru
`workflowActionTriggerSettings` acceptă patru câmpuri:
| Câmp | Scop |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `label` | Numele afișat pentru acțiune în selectorul de pași al constructorului de fluxuri de lucru. Implicit, este `name` al funcției. |
| `icon` | Pictograma afișată lângă acțiune (un nume `tabler-icons`, de ex. `IconBuilding`). |
| `inputSchema` | `InputSchema` avansat al Twenty — ceea ce constructorul afișează ca câmpuri configurabile (cu selectoare de variabile). Opțional; dedus din handler atunci când este omis. |
| `outputSchema` | Declară structura returnată de handler, astfel încât **pașii următori să poată mapa la câmpurile de ieșire ale acesteia**. Opțional; fără acesta, ieșirea este expusă ca o singură valoare opacă. |
Reunind totul — o funcție expusă ca o acțiune de flux de lucru, cu o ieșire declarată astfel încât pașii următori să poată face referire la `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' },
},
},
],
},
});
```
Odată ce aplicația este instalată, **Enrich Company** apare în selectorul de acțiuni al constructorului de fluxuri de lucru. Constructorul afișează `companyName` și `domain` ca câmpuri de intrare (fiecare putând prelua valori din pașii anteriori), iar pașii ulteriori pot face referire la ieșirile `taskId` și `enriched` ale pasului.
<Note>
**Scrieți o `description` bună.** Agenții AI se bazează pe câmpul `description` al funcției pentru a decide când să folosească instrumentul. Fiți specifici cu privire la ceea ce face instrumentul și când ar trebui apelat.
</Note>
</Accordion>
</AccordionGroup>
<Note>
**Ajutoare la rulare (runtime helpers).** `twenty-sdk/utils` re-exportă mici ajutoare la rulare, astfel încât handlerii să nu importe niciodată direct din `twenty-shared`. De exemplu, `isDefined(value)` returnează `false` atât pentru `null`, cât și pentru `undefined` — folosește-l pentru a restrânge în siguranță intrările opționale ale handlerilor, care pot ajunge drept `null` la rulare, chiar și atunci când sunt tipate `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>
**Hook-uri de instalare** — handleri pre-instalare, post-instalare și dezinstalare — partajează acest runtime, dar sunt declarați cu propriile lor funcții `define` și nu folosesc setări de declanșare. Consultați [Hook-uri de instalare](/l/ro/developers/extend/apps/config/install-hooks) pentru `definePreInstallLogicFunction`, `definePostInstallLogicFunction` și `defineUninstallLogicFunction`.
</Note>
## Creați o activitate pe cronologie.
Utilizați `createTimelineActivity()` pentru a publica un eveniment de domeniu explicit dintr-o funcție logică. Definiți mai întâi evenimentul ca [tip de activitate pe cronologie](/l/ro/developers/extend/apps/data/timeline-activity-types), apoi faceți referire la tip și la obiecte prin identificatorii lor universali stabili:
```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',
},
});
```
Twenty convertește identificatorii universali în ID-urile metadatelor specifice instalării, validează că tipul de activitate pe cronologie aparține aplicației apelante și captează metadatele sale de prezentare în noua activitate.
Parametrii de intrare obligatorii sunt `timelineActivityTypeUniversalIdentifier`, `targetObjectUniversalIdentifier` și `targetRecordId`. De asemenea, puteți furniza `happensAt`, `properties` și `workspaceMemberId`. `happensAt` controlează poziția și ora afișată a evenimentului pe cronologie; în mod implicit, este momentul creării.
Pentru a asocia o altă înregistrare cu evenimentul, furnizați împreună `linkedRecordId` și `linkedObjectMetadataUniversalIdentifier`. În plus, puteți furniza `linkedRecordCachedName` ca alternativă istorică pentru afișare:
```ts
await createTimelineActivity({
timelineActivityTypeUniversalIdentifier:
'f4fa646c-6e11-4d8f-a6be-c3b7a2fc7501',
targetObjectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
targetRecordId: postCardId,
linkedObjectMetadataUniversalIdentifier: RECIPIENT_UNIVERSAL_IDENTIFIER,
linkedRecordId: recipientId,
linkedRecordCachedName: recipientName,
});
```
Rolul funcției logice trebuie să aibă permisiunea de scriere pentru obiectul standard `timelineActivity`. Păstrați tipurile explicite de evenimente nelegate de o `action`; un tip legat de o acțiune primește deja evenimente de audit automate și, în caz contrar, ar produce rânduri duplicate.
## Clienți API tipizați (twenty-client-sdk)
Pachetul `twenty-client-sdk` oferă doi clienți GraphQL tipați pentru a interacționa cu API-ul Twenty din funcțiile de logică și componentele Front.
| Client | Importați | Endpoint | Generat? |
| ------------------- | ---------------------------- | ------------------------------------------------------------------- | ---------------------------- |
| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — date ale spațiului de lucru (înregistrări, obiecte) | Da, în timpul dev/build |
| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — configurarea spațiului de lucru, încărcări de fișiere | Nu, este livrat preconstruit |
<AccordionGroup>
<Accordion title="CoreApiClient" description="Interogați și modificați datele spațiului de lucru (înregistrări, obiecte)">
`CoreApiClient` este clientul principal pentru interogarea și modificarea datelor din spațiul de lucru. Este **generat din schema spațiului de lucru** în timpul `yarn twenty dev` sau `yarn twenty dev:build`, astfel încât este complet tipizat pentru a corespunde obiectelor și câmpurilor dvs.
```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,
},
});
```
Clientul folosește o sintaxă de tip selection-set: transmiteți `true` pentru a include un câmp, folosiți `__args` pentru argumente și imbricați obiecte pentru relații. Obțineți autocompletare și verificare completă a tipurilor, pe baza schemei spațiului dvs. de lucru.
<Note>
**CoreApiClient este generat în timpul dev/build.** Dacă îl utilizați fără a rula mai întâi `yarn twenty dev` sau `yarn twenty dev:build`, va arunca o eroare. Generarea are loc automat — CLI inspectează schema GraphQL a spațiului dvs. de lucru și generează un client tipizat folosind `@genql/cli`.
</Note>
#### Folosirea CoreSchema pentru adnotări de tip
`CoreSchema` oferă tipuri TypeScript care corespund obiectelor din spațiul dvs. de lucru — utile pentru tiparea stării componentelor sau a parametrilor funcțiilor:
```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ția spațiului de lucru, aplicații și încărcări de fișiere">
`MetadataApiClient` este livrat preconstruit împreună cu SDK-ul (nu este necesară generarea). Interoghează endpointul `/metadata` pentru configurarea spațiului de lucru, aplicații și încărcări de fișiere. Acceptă aceeași opțiune `runAs` ca `CoreApiClient` — consultați [Ale cui accesări le utilizează un apel](#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 },
},
},
});
```
#### Încărcarea fișierelor
`MetadataApiClient` include o metodă `uploadFile` pentru atașarea fișierelor la câmpuri de tip fișier:
```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://...' }
```
| Parametru | Tip | Descriere |
| ---------------------------------- | -------- | ------------------------------------------------------------------- |
| `fileBuffer` | `Buffer` | Conținutul brut al fișierului |
| `filename` | `string` | Numele fișierului (folosit pentru stocare și afișare) |
| `contentType` | `șir` | Tipul MIME (implicit `application/octet-stream` dacă este omis) |
| `fieldMetadataUniversalIdentifier` | `șir` | `universalIdentifier` al câmpului de tip fișier de pe obiectul dvs. |
Puncte cheie:
* Folosește `universalIdentifier` al câmpului (nu ID-ul specific spațiului de lucru), astfel încât codul dvs. de încărcare funcționează în orice spațiu de lucru în care aplicația dvs. este instalată.
* `url` returnat este un URL semnat pe care îl puteți folosi pentru a accesa fișierul încărcat.
</Accordion>
</AccordionGroup>
<Note>
Când codul dvs. rulează pe Twenty (funcții de logică sau componente Front), platforma injectează acreditările ca variabile de mediu:
* `TWENTY_API_URL` — URL-ul de bază al API-ului Twenty
* `TWENTY_APP_ACCESS_TOKEN` — Cheie cu durată scurtă pentru accesul implicit: rolul unei persoane se intersectează cu cel al aplicației dvs. atunci când cineva se află în spatele execuției, iar rolul propriu al aplicației dvs. atunci când nu se află nimeni. Persoana este cea care a declanșat execuția într-o funcție logică sau cea care privește pagina într-o componentă front-end.
* `TWENTY_APP_APPLICATION_ACCESS_TOKEN` — Cheie cu durată scurtă, limitată numai la rolul propriu al aplicației dvs. Numai funcții logice, injectată întotdeauna acolo și utilizată de `runAs: 'application'`.
Nu trebuie să transmiteți aceste valori clienților — aceștia citesc automat din `process.env`, iar [Ale cui accesări le utilizează un apel](#whose-access-a-call-uses) explică alegerea dintre ele. Permisiunile proprii ale aplicației dvs. sunt determinate de rolul declarat cu `defineApplicationRole()` (sau referențiat prin `defaultRoleUniversalIdentifier` în `application-config.ts`); o execuție care acționează ca o persoană nu poate depăși niciodată acel rol sau rolul persoanei.
</Note>

View File

@@ -0,0 +1,61 @@
---
title: Prezentare generală
description: TypeScript pe server care rulează în Twenty — declanșat de rute HTTP, programări cron, evenimente de bază de date, instrumente IA sau acțiuni de workflow.
icon: bolt
---
**Stratul de logică** al unei aplicații Twenty este codul care *rulează* — handler-e TypeScript pe server care reacționează la cereri HTTP, programări cron și modificări ale înregistrărilor; abilități și agenți IA care trăiesc în workspace; și conexiuni OAuth care permit funcțiilor tale să acționeze în numele utilizatorului în servicii terțe.
```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) │
└────────────────────────────┘
```
## În această secțiune
<CardGroup cols={2}>
<Card title="Funcții logice" icon="bolt" href="/l/ro/developers/extend/apps/logic/logic-functions">
Blocul de bază — tipuri de declanșatoare, payload-uri și clientul API cu tipuri.
</Card>
<Card title="Abilități și agenți" icon="robot" href="/l/ro/developers/extend/apps/logic/skills-and-agents">
Instrucțiuni reutilizabile pentru agenți IA și asistenți cu prompturi de sistem personalizate.
</Card>
<Card title="Conexiuni" icon="plug" href="/l/ro/developers/extend/apps/logic/connections">
Credențiale OAuth pe care aplicația ta le deține pentru servicii terțe — Linear, GitHub, Slack și altele.
</Card>
<Card title="Stocare cheie-valoare" icon="database" href="/l/ro/developers/extend/apps/logic/key-value-store">
Păstrează starea între execuțiile funcțiilor de logică — cache-uri, cursoare și revendicări între spații de lucru.
</Card>
<Card title="Sarcini în fundal" icon="layer-group" href="/l/ro/developers/extend/apps/logic/background-jobs">
Pune în coadă execuția unei funcții logice de către workeri pentru a depăși limita de timp per execuție.
</Card>
</CardGroup>
## Tipuri de declanșatoare, dintr-o privire
O funcție de logică alege unul sau mai multe declanșatoare — fiecare intrare de mai jos este un câmp separat pe `defineLogicFunction()`:
| Declanșator | Când rulează | Setare |
| ----------------------------- | ----------------------------------------------------------------- | ------------------------------- |
| **Rută HTTP** | O cerere ajunge la endpointul tău `/s/\<path>` | `httpRouteTriggerSettings` |
| **Cron** | O expresie CRON se potrivește | `cronTriggerSettings` |
| **Eveniment de bază de date** | O înregistrare din workspace este creată, actualizată sau ștearsă | `databaseEventTriggerSettings` |
| **Instrument IA** | O funcționalitate IA din Twenty decide să apeleze funcția ta | `toolTriggerSettings` |
| **Acțiune Workflow** | Un pas din workflow îți invocă funcția | `workflowActionTriggerSettings` |
Funcțiile rulează în sandbox, în procese Node.js izolate și accesează workspace-ul printr-un client API cu tipuri, limitat la rolul declarat în [`defineApplication()`](/l/ro/developers/extend/apps/config/application).
<Note>
**Hook-uri de instalare** — cod care rulează înainte sau după instalare — folosesc același runtime, dar au propriile lor funcții define și se află sub [Config → Install Hooks](/l/ro/developers/extend/apps/config/install-hooks).
</Note>

View File

@@ -0,0 +1,187 @@
---
title: Abilități și agenți
description: Definiți abilități și agenți AI pentru aplicația dvs.
icon: robot
---
<Warning>
Abilitățile și agenții sunt în prezent în stadiu alfa. Caracteristica funcționează, dar este încă în dezvoltare.
</Warning>
Aplicațiile pot defini capabilități AI care există în interiorul spațiului de lucru — instrucțiuni reutilizabile pentru abilități și agenți cu prompturi de sistem personalizate.
<AccordionGroup>
<Accordion title="defineSkill" description="Definiți abilități pentru agentul AI">
Abilitățile definesc instrucțiuni și capabilități reutilizabile pe care agenții AI le pot folosi în spațiul dvs. de lucru. Folosiți `defineSkill()` pentru a defini abilități cu validare încorporată:
```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`,
});
```
Puncte cheie:
* `name` este un șir identificator unic pentru abilitate (se recomandă kebab-case).
* `label` este numele lizibil afișat în interfața cu utilizatorul (UI).
* `content` conține instrucțiunile abilității — acesta este textul pe care agentul AI îl folosește.
* `icon` (opțional) setează pictograma afișată în UI.
* `description` (opțional) oferă context suplimentar despre scopul abilității.
</Accordion>
<Accordion title="defineAgent" description="Definiți agenți AI cu prompturi personalizate">
Agenții sunt asistenți AI care există în interiorul spațiului dvs. de lucru. Utilizați `defineAgent()` pentru a crea agenți cu un prompt de sistem personalizat:
```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.',
});
```
Puncte cheie:
* `name` este un șir identificator unic pentru agent (se recomandă kebab-case).
* `label` este numele de afișare din interfața cu utilizatorul (UI).
* `prompt` conține promptul de sistem — acesta este textul de instrucțiuni care definește comportamentul agentului.
* `description` (opțional) oferă context despre ce face agentul.
* `icon` (opțional) setează pictograma afișată în UI.
* `modelId` (opțional) suprascrie modelul AI implicit utilizat de agent.
* `responseFormat` (opțional) controlează forma ieșirii agentului. Valoarea implicită este `{ type: 'text' }` pentru text liber. Folosește `{ type: 'json', schema }` pentru a impune ieșire JSON structurată.
În mod implicit, un agent returnează text liber. Pentru a obține o ieșire structurată, setează `responseFormat` la `{ type: 'json' }` și furnizează `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,
},
},
});
```
Note despre schemă:
* Schema este un obiect plat: `type` al fiecărei proprietăți trebuie să fie un tip primitiv (`string`, `number` sau `boolean`). Obiectele îmbricate și tablourile nu sunt acceptate.
* `description` (opțional) pe fiecare proprietate ghidează modelul cu privire la ce să pună acolo.
* `required` (opțional) enumeră proprietățile pe care modelul trebuie să le returneze întotdeauna.
* `additionalProperties: false` (opțional) interzice orice proprietate care nu este declarată în `properties`.
</Accordion>
<Accordion title="runAgent" description="Rulează un agent dintr-o funcție logică">
`runAgent()` permite unei funcții logice să ruleze unul dintre agenții aplicației tale (cu abilitățile și instrumentele sale). Identifică agentul prin `universalIdentifier` pe care l-ai transmis către `defineAgent()`. Transmite fie un șir `prompt`, fie un istoric de conversație `messages`,
nu ambele:
```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.',
});
```
Pentru boți multi-turn (Slack, Discord, Teams, …), transmite istoricul firului de discuție ca
`messages` în loc de un singur `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?' },
],
});
```
Puncte cheie:
* Furnizează **exact unul** dintre `prompt` (șir) sau `messages` (1 până la 100 de intrări
de `{ role: 'user' | 'assistant', content: string }`).
* Agentul rulează **sincron** și poate citi/actualiza direct înregistrări prin propriile sale instrumente — `runAgent()` este rezolvat după ce rularea se încheie.
* O aplicație poate rula doar propriii săi agenți.
* [Rolul implicit](/l/ro/developers/extend/apps/config/roles) al aplicației trebuie să acorde indicatorul de permisiune `AI` — adaugă `SystemPermissionFlag.AI` la `permissionFlagUniversalIdentifiers` (sau setează `canAccessAllTools: true`).
Fără acesta, `runAgent()` eșuează cu o eroare de permisiune.
* Setează un `timeoutSeconds` generos pentru funcția logică — rulările agentului pot dura câteva secunde.
* `success` este `true` și `result` este nenul când rularea se încheie; în caz de eșec `success` este `false`, `result` este `null`, iar `error` conține motivul (de exemplu, când spațiul de lucru rămâne fără credite AI în timpul rulării).
```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>
**Evită buclele:** dacă apelezi `runAgent()` dintr-un declanșator de tip eveniment de bază de date `*.updated`, iar agentul actualizează aceeași înregistrare, restrânge declanșatorul cu `updatedFields` la un câmp pe care agentul nu îl scrie niciodată (de exemplu, URL-ul sursă) sau verifică dacă vreun câmp țintă este încă gol înainte de a apela `runAgent()`.
</Warning>
### Rulare în numele unui membru al spațiului de lucru
Transmite `runAsWorkspaceMemberId` atunci când rularea este declanșată de o persoană — de exemplu, un chatbot care răspunde la un mesaj — astfel încât agentul să acționeze ca acel membru în loc să acționeze ca aplicația:
```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',
});
```
Rularea acționează apoi cu **propriul rol al membrului**: poate face orice
poate face acel membru și nimic mai mult, înregistrările pe care le creează sunt atribuite lui, iar permisiunile lui la nivel de rând se aplică. Rolul agentului nu participă — este valoarea implicită a aplicației pentru rulările fără nicio persoană în spatele lor. Omite câmpul pentru
rulările autonome (joburi programate, declanșatoare de evenimente din baza de date): acelea păstrează propriul rol al agentului. Aplicația care acționează rămâne atașată la contextul rulării
pentru proveniență, fără a restrânge permisiunile membrului.
Aplicația ta este responsabilă să facă asocierea dintre persoana care a declanșat rularea și un membru al spațiului de lucru. A denumi unul necesită un token de acces al aplicației, iar ceea ce poate denumi un token depinde dacă acesta are sau nu un utilizator asociat:
* Un token **fără niciun utilizator atașat** poate denumi orice membru. O funcție logică rulează cu unul, la fel și tokenurile emise prin `client_credentials` sau dintr-o cheie API.
* Un token emis **în numele unui utilizator**, așa cum primește un front component, poate
denumi doar propriul membru al acelui utilizator.
Orice altceva — o sesiune simplă de utilizator, o cheie API fără un token al aplicației — nu poate denumi deloc un membru.
<Warning>
`runAgent()` aruncă o eroare atunci când membrul spațiului de lucru nu poate fi identificat — un membru necunoscut sau eliminat, ori unul fără niciun rol. Nu revine niciodată la
propriul rol al agentului, deoarece acest lucru ar acorda mai mult acces decât a cerut apelantul.
</Warning>
</Accordion>
</AccordionGroup>