Skip to main content
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.
Fiecare fișier de funcție folosește defineLogicFunction() pentru a exporta o configurație cu un handler și declanșatoare opționale.
src/logic-functions/createPostCard.logic-function.ts
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ă).
Pentru a apela o funcție logică declanșată de o rută dintr-o componentă front-end (headless), consultă Apelarea unei funcții logice.
  • 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.
Puteți, de asemenea, să executați manual o funcție folosind CLI:
Puteți urmări jurnalele cu:

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. Importați tipul RoutePayload din twenty-sdk/logic-function:
Tipul RoutePayload are următoarea structură:

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:
În handler, accesați anteturile transmise mai departe astfel:
Numele anteturilor sunt normalizate la litere mici. Accesați-le folosind chei cu litere mici (de exemplu, event.headers['content-type']).

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

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.

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ă.
src/logic-functions/resolve-server-route.logic-function.ts
src/logic-functions/handle-invoice-paid.logic-function.ts
Endpoint-ul este accesibil la:
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:
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:
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.
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.
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.
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.
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: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.
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.

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.
Payload-ul include: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.
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.
Exemplu de eveniment de creare:
Exemplu de eveniment de actualizare:
Declanșare doar la actualizări ale e-mailului:
Exemplu de eveniment de ștergere:

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ă:
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ă.
Contextul vă indică cine a declanșat execuția. Ceea ce poate face execuția este o declarație separată, mai jos.

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:
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:
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.
Ajutoarele SDK care accesează resursele proprii ale aplicației dvs. folosesc întotdeauna accesul acesteia și ignoră runAs: stocarea cheie-valoare, conexiunile, 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ă.
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 în ghidul utilizatorului pentru vizualizarea din perspectiva utilizatorului final.
src/logic-functions/enrich-company.logic-function.ts
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:
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:
Un exemplu complet de acțiune de flux de lucru
workflowActionTriggerSettings acceptă patru câmpuri: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:
src/logic-functions/enrich-company.logic-function.ts
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.
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.
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:
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 pentru definePreInstallLogicFunction, definePostInstallLogicFunction și defineUninstallLogicFunction.

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, apoi faceți referire la tip și la obiecte prin identificatorii lor universali stabili:
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:
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.
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.
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.
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.

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

Încărcarea fișierelor

MetadataApiClient include o metodă uploadFile pentru atașarea fișierelor la câmpuri de tip fișier:
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.
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 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.