Vai al contenuto principale

Panoramica

Le skill e gli agenti sono attualmente in fase alfa. La funzionalità funziona ma è ancora in evoluzione.
Le app possono definire capacità di IA che risiedono all’interno dello spazio di lavoro — istruzioni di skill riutilizzabili e agenti con prompt di sistema personalizzati.
Le skill definiscono istruzioni e capacità riutilizzabili che gli agenti IA possono utilizzare all’interno del tuo spazio di lavoro. Usa defineSkill() per definire skill con convalida integrata:
src/skills/example-skill.ts
Punti chiave:
  • name è una stringa identificativa univoca per la skill (kebab-case consigliato).
  • label è il nome di visualizzazione leggibile mostrato nell’UI.
  • content contiene le istruzioni della skill — questo è il testo che l’agente IA utilizza.
  • icon (opzionale) imposta l’icona visualizzata nell’UI.
  • description (opzionale) fornisce contesto aggiuntivo sullo scopo della skill.
Gli agenti sono assistenti IA che vivono all’interno del tuo spazio di lavoro. Usa defineAgent() per creare agenti con un prompt di sistema personalizzato:
src/agents/example-agent.ts
Punti chiave:
  • name è la stringa identificativa univoca dell’agente (kebab-case consigliato).
  • label è il nome visualizzato nell’UI.
  • prompt è il prompt di sistema che definisce il comportamento dell’agente.
  • description (opzionale) fornisce contesto su ciò che fa l’agente.
  • icon (opzionale) imposta l’icona visualizzata nell’UI.
  • modelId (opzionale) sostituisce il modello di IA predefinito utilizzato dall’agente.
  • responseFormat (facoltativo) imposta il formato di output predefinito dell’agente. Per il testo in formato libero, il valore predefinito è { type: 'text' }. Usa { type: 'json', schema } per forzare un output JSON strutturato. Una chiamata a runAgent() o un passaggio AI di un workflow può richiedere un formato diverso, che ha la precedenza.
  • roleUniversalIdentifier (facoltativo) associa l’agente a un ruolo definito dalla tua app. Quel ruolo non deve concedere più del ruolo dell’applicazione, altrimenti la build non riesce; vedi Ruoli degli agenti.
  • modelConfiguration (facoltativo) attiva gli strumenti nativi del modello, come fa la pagina delle impostazioni dell’agente: { webSearch: { enabled: true }, twitterSearch: { enabled: true } }. Ogni strumento funziona solo con i modelli che lo supportano. Omettilo per mantenerli disattivati.
  • isSystem (facoltativo, il valore predefinito è false) nasconde l’agente dagli elenchi di agenti, come gli agenti che Twenty crea per i passaggi IA dei workflow. Modifica solo ciò che viene visualizzato: gli agenti di un’app possono essere modificati solo tramite l’app, indipendentemente da quanto indica questo flag.
Per impostazione predefinita, un agente restituisce testo in formato libero. Per ottenere un output strutturato, imposta responseFormat su { type: 'json' } e fornisci uno schema:
src/agents/structured-agent.ts
Note sullo schema:
  • Lo schema è un oggetto piatto: il type di ogni proprietà deve essere un primitivo (string, number o boolean). Gli oggetti annidati e gli array non sono supportati.
  • description (facoltativo) su ogni proprietà guida il modello su cosa inserire lì.
  • required (facoltativo) elenca le proprietà che il modello deve sempre restituire.
  • additionalProperties: false (facoltativo) vieta qualsiasi proprietà non dichiarata in properties.
I server MCP sono in beta e non sono ancora attivi: puoi dichiararli, ma gli spazi di lavoro non possono usare i loro strumenti finché la funzionalità non verrà rilasciata.
Usa defineMcpServer() per distribuire un server MCP remoto (Streamable HTTP) con la tua app, in modo che l’IA dello spazio di lavoro possa usare i suoi strumenti senza sincronizzare tali dati in Twenty:
src/mcp-servers/github.ts
Punti chiave:
  • name è in snake_case (lettere minuscole, cifre e trattini bassi, inizia con una lettera, massimo 32 caratteri) ed è univoco nello spazio di lavoro. Funge da prefisso per i nomi degli strumenti del server.
  • url deve essere un URL HTTP o HTTPS.
  • connectionProvider (facoltativo) fa riferimento a un provider di connessione dichiarato dalla stessa app. Le chiamate inviano il token di accesso di tale connessione come token Bearer. Omettilo per un server pubblico.
  • L’URL non può cambiare in una versione successiva quando è collegato un provider di connessione, affinché i token non vengano mai inviati a un altro host.
  • Gli spazi di lavoro possono disattivare il server; una versione successiva dell’app mantiene questa scelta.
  • Richiede "engines": { "twenty": ">=2.53.0" }.
runAgent() permette a una funzione di logica di eseguire uno degli agenti della tua app (con le sue skill e i suoi strumenti). Identifica l’agente tramite l’universalIdentifier che hai passato a defineAgent() e passa ciò su cui vuoi che lavori come input:
src/logic-functions/run-enricher.ts
Punti chiave:
  • input è una stringa o un elenco da 1 a 100 messaggi ({ role: 'user' | 'assistant', content: string }). Un messaggio user può anche includere allegati di file (vedi sotto). Per mantenere una conversazione, passa un thread invece di riprodurre tu stesso i turni precedenti.
  • Per impostazione predefinita, la chiamata attende l’agente, che può leggere/aggiornare i record autonomamente tramite i propri strumenti — runAgent() viene risolta una volta completata l’esecuzione. Passa waitForCompletion: false per risolvere non appena l’esecuzione si avvia: status è quindi RUNNING e result: null, e la risposta arriva nella conversazione threadId. Un’esecuzione che supera i 15 minuti restituisce anch’essa RUNNING.
  • L’agente può anche sospendersi finché un record non cambia o non è trascorso del tempo. runAgent() viene quindi risolto con success: true, status: 'SUSPENDED' e result: null; l’agente prosegue autonomamente in seguito e la sua risposta arriva nella conversazione threadId. Una chiamata a un thread il cui agente è ancora in attesa non riesce finché non termina.
  • Ogni chiamata viene archiviata con il relativo input e stato, oltre alla risposta e all’utilizzo al completamento, così da comparire nelle esecuzioni dell’agente. Senza un thread, una chiamata riceve una conversazione propria: threadId la denomina e una chiamata successiva non la vede mai.
  • Un’app può eseguire solo i propri agenti.
  • Il ruolo predefinito dell’app deve concedere il flag di autorizzazione AI — aggiungi SystemPermissionFlag.AI ai suoi permissionFlagUniversalIdentifiers (oppure imposta canAccessAllTools: true). In mancanza di ciò, runAgent() non riesce con un errore di autorizzazione.
  • Imposta un valore generoso per timeoutSeconds sulla funzione di logica — l’esecuzione degli agenti può richiedere diversi secondi.
  • status è COMPLETED con un result non null quando l’esecuzione viene completata senza attendere; SUSPENDED con success: true e result: null mentre attende; RUNNING con success: true e result: null quando la chiamata non ha atteso l’esecuzione; FAILED con success: false e error che contiene il motivo (ad esempio, quando lo spazio di lavoro esaurisce i crediti AI durante l’esecuzione).
src/roles/default-role.ts
Evita i loop: se chiami runAgent() da un trigger di evento del database *.updated e l’agente aggiorna lo stesso record, limita il trigger con updatedFields a un campo che l’agente non scrive mai (ad esempio l’URL di origine), oppure controlla che almeno uno dei campi di destinazione sia ancora vuoto prima di chiamare runAgent().

Invio di file con un messaggio

Un messaggio user può includere attachments, quindi l’agente vede uno screenshot o un PDF anziché solo il relativo nome. Ogni voce fa riferimento a un file che la tua app ha già caricato nella cartella agent-chat:
src/logic-functions/describe-screenshot.ts
Il caricamento richiede il flag di autorizzazione UPLOAD_FILE nel ruolo predefinito dell’app, insieme ad AI. filename è facoltativo e serve solo a etichettare il file per il modello.
  • Solo i messaggi user possono contenere allegati. Non possono essere rappresentati in un turno dell’assistente, quindi runAgent() li rifiuta lì anziché eliminarli.
  • Al massimo 10 allegati per messaggio.
  • Il fileId deve indicare un file completamente caricato nello stesso spazio di lavoro, nella cartella agent-chat. Qualsiasi altra cosa fa fallire la chiamata anziché rispondere silenziosamente senza il file.

Richiedere un risultato strutturato

Passa responseFormat per ottenere un formato solo per questa chiamata. Ha la precedenza su responseFormat dell’agente stesso, quindi un agente può servire chiamanti che necessitano di output diversi. Utilizza lo stesso schema di defineAgent():
src/logic-functions/score-lead.ts

Proseguire una conversazione

Passa un thread e Twenty mantiene la conversazione per te: ogni chiamata invia solo il nuovo input e l’agente vede i turni precedenti, inclusi i risultati restituiti dai suoi strumenti. Usa come chiave qualsiasi identificatore stabile dal tuo lato, ad esempio un canale Slack e il timestamp del thread:
src/logic-functions/reply-in-thread.ts
  • Le chiavi hanno ambito limitato alla tua app e al tuo agente, quindi due app non condividono mai una conversazione. Solo i token di accesso dell’app possono passare un thread.
  • input può contenere solo messaggi user: la conversazione contiene già le risposte dell’agente.
  • additionalInstructions vengono fornite al modello solo per questa chiamata e non vengono archiviate, il che è adatto per indicazioni specifiche per richiesta, come regole di formattazione o una scadenza. Funzionano anche senza un thread.
  • Ogni esecuzione viene archiviata come turno con il relativo stato, incluse quelle non riuscite. Il suo utilizzo viene archiviato quando l’agente restituisce il controllo, anche se ha esaurito i crediti. Un’ esecuzione non riuscita non fa parte della conversazione, quindi la chiamata successiva non la vede.
  • Le chiamate sullo stesso thread vengono eseguite una alla volta: una seconda chiamata attende che la prima termini, così ciascuna vede i turni precedenti.
  • Quando i turni vengono eseguiti come membri diversi (vedi sotto), ciascuno vede solo il testo dei turni degli altri, mai i risultati dei loro strumenti, poiché questi sono stati letti con le autorizzazioni di qualcun altro.

Esecuzione per conto di un membro dello spazio di lavoro

Passa runAsWorkspaceMemberId quando l’esecuzione viene attivata da una persona — ad esempio un chatbot che risponde a un messaggio — in modo che l’agente agisca come quel membro invece che come l’app:
src/logic-functions/answer-question.ts
L’esecuzione quindi agisce con il ruolo proprio del membro: può fare tutto ciò che quel membro può fare e niente di più, i record che crea sono attribuiti a lui e si applicano i suoi permessi a livello di riga. Il ruolo dell’agente non partecipa — è l’impostazione predefinita dell’app per le esecuzioni senza nessuno dietro. Ometti il campo per le esecuzioni autonome (attività pianificate, trigger di eventi del database): queste mantengono il ruolo proprio dell’agente. L’applicazione che agisce rimane collegata al contesto dell’esecuzione per la tracciabilità, senza restringere i permessi del membro.La tua app è responsabile di mappare la persona che ha attivato l’esecuzione a un membro dello spazio di lavoro. Per specificarne uno è necessario un token di accesso dell’app e ciò che un token può specificare dipende dal fatto che sia o meno associato a un utente:
  • Un token senza alcun utente associato può specificare qualsiasi membro. Una funzione logica viene eseguita con uno, e così anche i token generati tramite client_credentials o da una chiave API.
  • Un token emesso per conto di un utente, come quello che riceve un front component, può specificare solo il membro dell’utente stesso.
Qualsiasi altra cosa — una semplice sessione utente, una chiave API senza un token dell’app — non può specificare affatto un membro.
runAgent() genera un errore quando il membro dello spazio di lavoro non può essere risolto — un membro sconosciuto o rimosso, o uno senza ruolo. Non ricorre mai al ruolo proprio dell’agente, poiché ciò concederebbe più accesso di quanto richiesto dal chiamante.

Seguire una conversazione

Un’app legge le conversazioni che intrattiene con i propri agenti tramite la stessa API della chat: listConversationMessages() restituisce la relativa cronologia e streamConversation() produce i relativi eventi in tempo reale, token per token. Assegna un nome alla conversazione tramite l’agente e la chiave thread che passi a runAgent(); puoi iniziare a seguirla prima che la prima esecuzione la raggiunga.
src/logic-functions/follow-conversation.ts
  • Gli eventi sono quelli della chat stessa: stream-chunk contiene un UIMessageChunk dell’AI SDK e un seq, e notifiche quali message-persisted (un messaggio è stato salvato: quello di un membro o la risposta, che contiene il proprio turnId), credits-exhausted (la risposta si è interrotta quando lo spazio di lavoro ha esaurito i crediti), queue-updated o stream-error indicano quando leggere nuovamente la cronologia. Un keepalive arriva una volta stabilita la connessione, poi ogni pochi secondi: leggi la cronologia al primo, poiché ciò che è accaduto prima si trova lì.
  • Per trasformare i chunk in messaggi come fa la chat, inviali a readUIMessageStream() dal pacchetto ai.
  • listConversationMessages() restituisce { messages, streamCatchup }: i messaggi salvati come UIMessage dell’AI SDK, come li costruisce la chat, e i chunk di una risposta ancora in streaming. Mostra tali chunk, poi ignora i chunk in tempo reale il cui seq è al massimo streamCatchup.maxSeq, poiché li ripetono.
  • runAgent() restituisce runId, il turno aperto dalla chiamata: i messaggi di tale esecuzione lo contengono come metadata.turnId. Un’esecuzione in attesa prosegue nei turni successivi della conversazione.
  • Lo stream rimane aperto finché non interrompi il relativo signal o usi break, e genera un errore se la connessione cade: avvia nuovamente lo streaming per riprendere.
  • Un’app legge solo le proprie conversazioni. I turni che un membro ha aperto altrove, ad esempio nella chat, mostrano solo il proprio testo, nella cronologia, in tempo reale e nella successiva esecuzione dell’agente: in tempo reale, gli altri chunk arrivano come chunk message-metadata vuoti, così seq rimane senza lacune. I turni eseguiti dalla tua app come membro (vedi sopra) rimangono integri quando li legge autonomamente.
  • Richiede "engines": { "twenty": ">=2.56.0" }.
ai.generate() chiama un modello da una funzione logica. A differenza di runAgent(), non ha bisogno di defineAgent(): la chiamata porta le proprie istruzioni e non apre conversazioni. Usalo per un lavoro one-shot come la classificazione, la sintesi o l’estrazione di .
  • input è una stringa o un elenco di messaggi { role: 'user' | 'assistant', content }.
  • instructions (opzionale) aggiunge le istruzioni di sistema del chiamante.
  • modelId (opzionale) sceglie il modello. Senza di esso, la chiamata utilizza il modello predefinito del workspace .
  • responseFormat (opzionale) funziona come in runAgent(). Con { type: 'json', schema }, lo schema va sulla richiesta del modello stesso, così una risposta JSON richiede una richiesta (più passaggi dello strumento quando un scope concede strumenti ).
  • scope (opzionale) consente alla chiamata di utilizzare il database, come descritto di seguito.
Si risolve a { successo, testo, oggetto, errore, runId, use }:
  • text è la risposta di una risposta testuale e null per una JSON.
  • object è il risultato di una risposta JSON, e null altrimenti.
  • error spiega un’esecuzione fallita, quando success è false. Una corsa che fallisce risolve questo modo piuttosto che lanciare; una chiamata che non può avviare un’esecuzione, come come una con input non valido, lancia.
  • runId identifica l’esecuzione registrata, con il suo modello, utilizzo e crediti.
  • usage è { inputTokens, outputTokens }: quello che è stato speso, inclusi i passaggi che sono terminati prima di un fallimento.
La chiamata agisce come chi lo ha fatto: il membro a cui appartiene un token di ambito utente, altrimenti la tua applicazione, altrimenti la chiave API. Da una funzione logica segue i client predefiniti: agisce come la persona che ha innescato l’esecuzione, o come la tua applicazione quando nessuno lo ha fatto. Non supera mai il ruolo della tua applicazione, e un’applicazione senza un ruolo predefinito non può chiamarla.Senza scope, la chiamata legge e non scrive alcun record. Per permettergli di utilizzare il database , passare un scope, scritto come permessi di un ruolo:
Un ambito si restringe soltanto: non concede nulla che il chiamante non abbia già.Per mostrare la risposta mentre è scritta, passare onChunk. La chiamata risolve ancora allo stesso risultato, una volta che l’ultimo pezzo è stato consegnato a onChunk. Se la fine del flusso non arriva mai, smette di aspettare una volta che il risultato è in e 5 s passare senza un nuovo chunk, e ancora consegna su ogni pezzo ricevuto da allora:
  • La chiamata segue la corsa attraverso lo stesso abbonamento di streamConversation(): si iscrive prima, aspetta fino a quando l’abbonamento è live, poi inizia la run, quindi non manca nessun chunk.
  • I chunks sono di proprietà della chat: li incanalano in readUIMessageStream() dal pacchetto ai per costruire il messaggio.
  • signal interrompe la chiamata: rifiuta con l’errore di abort e chiude il flusso . La corsa stessa continua, e viene registrata come al solito.
  • Una volta che lo stream è live, un guasto di esso ferma solo i chunk: la chiamata si risolve ancora con il risultato della corsa, che è fatturata in entrambi i modi. La chiamata rifiuta quando l’abbonamento fallisce prima che vada in diretta, quindi la corsa non inizia mai, o quando onChunk lancia.
  • Un’app segue solo le sue funzioni, quindi onChunk ha bisogno di un token di app.
  • Richiede "engines": { "twenty": ">=2.56.0" }.
ai.generate() richiede il flag dei permessi AI sul ruolo predefinito dell’app, come runAgent().
sendInboxMessage() pubblica un messaggio dalla tua app nelle chat di uno o più membri dello spazio di lavoro. Può terminare con una chiamata di strumento a cui il membro risponde dalla chat, dopo la quale la conversazione continua con l’assistente di Twenty. Consulta Posta in arrivo per le relative opzioni, gli helper per le menzioni e come pubblicare da un componente front-end.