Saltar al contenido principal

Resumen

Las habilidades y los agentes están actualmente en pruebas alfa. La funcionalidad es operativa, pero sigue evolucionando.
Las aplicaciones pueden definir capacidades de IA que residen dentro del espacio de trabajo — instrucciones de habilidades reutilizables y agentes con prompts de sistema personalizados.
Las habilidades definen instrucciones y capacidades reutilizables que los agentes de IA pueden usar dentro de tu espacio de trabajo. Usa defineSkill() para definir habilidades con validación incorporada:
src/skills/example-skill.ts
Puntos clave:
  • name es una cadena identificadora única de la habilidad (se recomienda kebab-case).
  • label es el nombre para mostrar, legible para humanos, que aparece en la interfaz de usuario.
  • content contiene las instrucciones de la habilidad — este es el texto que usa el agente de IA.
  • icon (opcional) establece el icono mostrado en la interfaz de usuario.
  • description (opcional) proporciona contexto adicional sobre el propósito de la habilidad.
Los agentes son asistentes de IA que viven dentro de tu espacio de trabajo. Usa defineAgent() para crear agentes con un prompt de sistema personalizado:
src/agents/example-agent.ts
Puntos clave:
  • name es una cadena identificadora única del agente (se recomienda kebab-case).
  • label es el nombre para mostrar que aparece en la interfaz de usuario.
  • prompt es el mensaje del sistema que define el comportamiento del agente.
  • description (opcional) proporciona contexto sobre lo que hace el agente.
  • icon (opcional) establece el icono mostrado en la interfaz de usuario.
  • modelId (opcional) reemplaza el modelo de IA predeterminado usado por el agente.
  • responseFormat (opcional) establece la estructura de salida predeterminada del agente. De forma predeterminada es { type: 'text' } para texto de formato libre. Usa { type: 'json', schema } para forzar una salida JSON estructurada. Una llamada a runAgent() o un paso de IA de un flujo de trabajo puede solicitar una estructura diferente, que tiene prioridad.
  • roleUniversalIdentifier (opcional) vincula el agente a un rol que define tu aplicación. Ese rol no debe otorgar más que el rol de la aplicación, o la compilación falla; consulta Roles de agente.
  • modelConfiguration (opcional) activa las herramientas nativas del modelo, como lo hace la página de configuración del agente: { webSearch: { enabled: true }, twitterSearch: { enabled: true } }. Cada herramienta solo funciona con los modelos que la admiten. Omítelo para mantenerlas desactivadas.
  • isSystem (opcional, el valor predeterminado es false) oculta el agente de las listas de agentes, como los agentes que Twenty crea para los pasos de IA de flujo de trabajo. Solo cambia lo que se muestra: los agentes de una aplicación solo se pueden cambiar mediante la aplicación, independientemente de lo que indique esta marca.
De forma predeterminada, un agente devuelve texto de formato libre. Para obtener una salida estructurada, establece responseFormat en { type: 'json' } y proporciona un schema:
src/agents/structured-agent.ts
Notas sobre el esquema:
  • El esquema es un objeto plano: el type de cada propiedad debe ser un tipo primitivo (string, number o boolean). Los objetos anidados y los arrays no son compatibles.
  • description (opcional) en cada propiedad guía al modelo sobre qué debe poner allí.
  • required (opcional) enumera las propiedades que el modelo siempre debe devolver.
  • additionalProperties: false (opcional) prohíbe cualquier propiedad que no esté declarada en properties.
Los servidores MCP están en beta y aún no están disponibles: puedes declararlos, pero los espacios de trabajo no pueden usar sus herramientas hasta que se lance la función.
Usa defineMcpServer() para incluir un servidor MCP remoto (HTTP transmisible) con tu aplicación, para que la IA del espacio de trabajo pueda usar sus herramientas sin sincronizar esos datos con Twenty:
src/mcp-servers/github.ts
Puntos clave:
  • name está en snake_case (letras minúsculas, dígitos y guiones bajos, comienza con una letra y tiene como máximo 32 caracteres) y es único en el espacio de trabajo. Sirve de prefijo para los nombres de las herramientas del servidor.
  • url debe ser una URL HTTP o HTTPS.
  • connectionProvider (opcional) hace referencia a un proveedor de conexión declarado por la misma aplicación. Las llamadas envían el token de acceso de esa conexión como un token Bearer. Omítelo para un servidor público.
  • La URL no puede cambiar en una versión posterior mientras haya un proveedor de conexión vinculado, por lo que los tokens nunca se envían a otro host.
  • Los espacios de trabajo pueden desactivar el servidor; una versión posterior de la aplicación conserva esa elección.
  • Requiere "engines": { "twenty": ">=2.53.0" }.
runAgent() permite que una función de lógica ejecute uno de los agentes de tu app (con sus skills y tools). Identifica al agente mediante el universalIdentifier que pasaste a defineAgent() y pasa lo que quieres que procese como input:
src/logic-functions/run-enricher.ts
Puntos clave:
  • input es una cadena o una lista de 1 a 100 mensajes ({ role: 'user' | 'assistant', content: string }). Un mensaje de user también puede incluir archivos adjuntos (consulta más abajo). Para mantener una conversación, pasa un thread en lugar de reproducir tú mismo los turnos anteriores.
  • De forma predeterminada, la llamada espera al agente, que puede leer/actualizar registros por sí mismo mediante sus propias herramientas — runAgent() se resuelve una vez que finaliza la ejecución. Pase waitForCompletion: false para que se resuelva en cuanto se inicie la ejecución: status será entonces RUNNING y result: null, y la respuesta llegará a la conversación de threadId. Una ejecución que dure más de 15 minutos también se resuelve como RUNNING.
  • El agente también puede pausarse hasta que cambie un registro o haya transcurrido algún tiempo. runAgent() se resuelve entonces con success: true, status: 'SUSPENDED' y result: null; el agente continúa por sí solo más tarde, y su respuesta llega a la conversación threadId. Una llamada a un thread cuyo agente sigue esperando falla hasta que termina.
  • Cada llamada se almacena con su entrada y estado, además de su respuesta y uso cuando se completa, por lo que aparece en las ejecuciones del agente. Sin un thread, una llamada obtiene una conversación propia: threadId la denomina, y una llamada posterior nunca la ve.
  • Una app solo puede ejecutar sus propios agentes.
  • El rol predeterminado de la app debe conceder el indicador de permiso AI; agrega SystemPermissionFlag.AI a sus permissionFlagUniversalIdentifiers (o establece canAccessAllTools: true). Sin esto, runAgent() falla con un error de permisos.
  • Establece un valor generoso de timeoutSeconds en la función de lógica: las ejecuciones de agentes pueden tardar varios segundos.
  • status es COMPLETED con un result distinto de nulo cuando la ejecución finaliza sin esperar; SUSPENDED con success: true y result: null mientras espera; RUNNING con success: true y result: null cuando la llamada no esperó a la ejecución; FAILED con success: false y error que contiene el motivo (por ejemplo, cuando el espacio de trabajo se quedó sin créditos de IA durante la ejecución).
src/roles/default-role.ts
Evita los bucles: si llamas a runAgent() desde un trigger de evento de base de datos *.updated y el agente actualiza el mismo registro, limita el alcance del trigger con updatedFields a un campo que el agente nunca escriba (por ejemplo, la URL de origen), o comprueba si algún campo de destino sigue vacío antes de llamar a runAgent().

Envío de archivos con un mensaje

Un mensaje user puede incluir attachments, para que el agente vea una captura de pantalla o un PDF en lugar de solo su nombre. Cada entrada hace referencia a un archivo que tu aplicación ya ha subido a la carpeta agent-chat:
src/logic-functions/describe-screenshot.ts
La carga requiere la marca de permiso UPLOAD_FILE en el rol predeterminado de la aplicación, junto con AI. filename es opcional y solo etiqueta el archivo para el modelo.
  • Solo los mensajes de user pueden incluir archivos adjuntos. No se pueden representar en un turno de asistente, por lo que runAgent() los rechaza allí en lugar de omitirlos.
  • Como máximo, 10 archivos adjuntos por mensaje.
  • El fileId debe nombrar un archivo completamente cargado en el mismo espacio de trabajo, dentro de la carpeta agent-chat. Cualquier otra cosa hace que la llamada falle en lugar de responder silenciosamente sin el archivo.

Solicitar un resultado estructurado

Pasa responseFormat para obtener una estructura solo para esta llamada. Tiene prioridad sobre el propio responseFormat del agente, por lo que un agente puede atender a quienes necesitan resultados diferentes. Utiliza el mismo esquema que defineAgent():
src/logic-functions/score-lead.ts

Continuar una conversación

Pasa un thread y Twenty mantiene la conversación por ti: cada llamada envía solo la nueva entrada, y el agente ve los turnos anteriores, incluidos los resultados que devolvieron sus herramientas. Usa como clave cualquier identificador estable de tu lado, como un canal de Slack y una marca de tiempo de hilo:
src/logic-functions/reply-in-thread.ts
  • Las claves están limitadas a tu aplicación y agente, por lo que dos aplicaciones nunca comparten una conversación. Solo los tokens de acceso de la aplicación pueden pasar un thread.
  • input solo puede contener mensajes de user: la conversación ya contiene las respuestas del agente.
  • Las additionalInstructions se proporcionan al modelo solo para esta llamada y no se almacenan, lo que es adecuado para instrucciones por solicitud, como reglas de formato o una fecha límite. También funcionan sin un thread.
  • Cada ejecución se almacena como un turno con su estado, incluidas las que fallaron. Su uso se almacena cuando el agente regresa, incluso si se quedó sin créditos. Una ejecución fallida no forma parte de la conversación, por lo que la siguiente llamada no la ve.
  • Las llamadas en el mismo hilo se ejecutan de una en una: una segunda llamada espera a que termine la primera, por lo que cada una ve los turnos anteriores a ella.
  • Cuando los turnos se ejecutan como miembros diferentes (consulta más abajo), cada uno solo ve el texto de los turnos de los demás, nunca los resultados de sus herramientas, ya que se leyeron con los permisos de otra persona.

Ejecución en nombre de un miembro del espacio de trabajo

Pasa runAsWorkspaceMemberId cuando la ejecución es activada por una persona — por ejemplo, un bot de chat que responde a un mensaje — para que el agente actúe como ese miembro en lugar de como la aplicación:
src/logic-functions/answer-question.ts
La ejecución entonces actúa con el propio rol del miembro: puede hacer todo lo que ese miembro puede hacer y nada más, los registros que crea se le atribuyen a él, y se aplican sus permisos a nivel de fila. El rol del agente no participa: es el valor predeterminado de la aplicación para ejecuciones sin nadie detrás de ellas. Omite el campo para ejecuciones autónomas (trabajos programados, disparadores de eventos de base de datos): esos conservan el rol propio del agente. La aplicación que actúa permanece asociada al contexto de la ejecución para fines de trazabilidad, sin restringir los permisos del miembro.Tu aplicación es responsable de asociar a la persona que activó la ejecución con un miembro del espacio de trabajo. Para poder designar uno se requiere un token de acceso de la aplicación, y a quién puede designar un token depende de si lleva asociado un usuario:
  • Un token sin usuario asociado puede designar a cualquier miembro. Una función lógica se ejecuta con uno, y lo mismo ocurre con los tokens generados mediante client_credentials o a partir de una clave de API.
  • Un token emitido en nombre de un usuario, como el que recibe un componente de interfaz, solo puede designar al miembro propio de ese usuario.
Cualquier otro caso — una sesión de usuario simple, una clave de API sin un token de la aplicación — no puede designar a ningún miembro.
runAgent() produce un error cuando no se puede resolver el miembro del espacio de trabajo — un miembro desconocido o eliminado, o uno sin rol. Nunca recurre al rol propio del agente, ya que eso otorgaría más acceso del que solicitó quien llama.

Siguiendo una conversación

Una aplicación lee las conversaciones que mantiene con sus agentes a través de la misma API que el chat: listConversationMessages() devuelve su historia, y streamConversation() produce sus eventos en vivo, token por token. Nombra la conversación por el agente y la clave hilo que pasas a runAgent(); Tú puedes empezar a seguirlo antes de que la primera ejecución llegue a él.
src/logic-functions/follow-conversation.ts
  • Los eventos son propios del chat: stream-chunk lleva un SDK UIMessageChunk y un seq, y notificaciones como message-persisted (un mensaje fue guardado: un miembro, o la respuesta, que lleva su ejecución turnId), credits-exhausted (la respuesta se detuvo cuando el espacio de trabajo corrió sin créditos), queue-updated o stream-error decir cuando para leer la historia de nuevo. Un keepalive viene una vez conectado, luego cada pocos segundos: lee la historia en la primera, ya que lo que pasó antes de está ahí.
  • Para convertir chunks en mensajes como lo hace el chat, pipéalos en readUIMessageStream() del paquete ai.
  • listConversationMessages() devuelve { mensajes, streamCatchup }: los mensajes guardados como AI SDK UIMessages, a medida que el chat los construye, y las porciones de una respuesta todavía fluyendo. Muestra esos trozos y luego omite trozos vivos cuyos seq es como máximo streamCatchup.maxSeq, ya que los repiten.
  • runAgent() devuelve runId, el giro que abre la llamada: los mensajes de esa ejecución llevan como metadata.turnId. Una ejecución que espera en turnos posteriores de la conversación.
  • El stream permanece abierto hasta que abortes su señal o romper, y arroja si la conexión cae: stream de nuevo para reanudar.
  • Una aplicación sólo lee sus propias conversaciones. Turna un miembro abierto en otro lugar, como en el chat, muestra solo su texto en la historia, viva y en la siguiente carrera del agente : vive, sus otros trozos llegan como trozos message-metadata, así que seq permanece recolectado. Turna tu aplicación se ha ejecutado como un miembro (ver arriba) permanecen entera cuando lee por sí mismo.
  • Requiere "engines": { "twenty": ">=2.56.0" }.
ai.generate() llama a un modelo desde una función lógica. A diferencia de runAgent(), no necesita defineAgent(): la llamada lleva sus propias instrucciones , y no abre ninguna conversación. Úselo para un trabajo de un solo tiro, como clasificar, resumir o extraer.
  • input es una cadena o una lista de mensajes { role: 'user' | 'assistant', content } .
  • instructions (opcional) añade las instrucciones del sistema de llamada.
  • modelId (opcional) elige el modelo. Sin ella, la llamada utiliza el modelo predeterminado del espacio de trabajo .
  • responseFormat (opcional) funciona como en runAgent(). Con { type: 'json', schema }, el esquema se incluye en la propia solicitud al modelo, por lo que una respuesta JSON requiere una solicitud (más pasos de herramientas cuando un scope concede herramientas).
  • scope (opcional) permite a la llamada utilizar la base de datos, como se describe a continuación.
Resuelve a { éxito, texto, objeto, error, runId, uso }:
  • text es la respuesta de un texto, y null para una respuesta JSON.
  • object es el resultado de una respuesta JSON, y null de otra manera.
  • error explica una ejecución fallida, cuando success es false. Una ejecución que falla resuelve esta manera en lugar de lanzarla; una llamada que no puede iniciar una ejecución, tal como una con entrada no válida, lanza.
  • runId identifica la ejecución registrada, con su modelo, uso y créditos.
  • usage es { inputTokens, outputTokens }: lo que gastó la ejecución, incluyendo los pasos que terminaron antes de un fallo.
La llamada actúa como quien lo hizo: el miembro a un token con ámbito de usuario pertenece, de lo contrario su aplicación, de lo contrario la clave API. Desde una función lógica sigue a los clientes predeterminados: actúa como la persona que desencadenó la ejecución, o como tu aplicación cuando nadie lo hizo. Nunca excede el rol de tu aplicación, y una aplicación sin un rol predeterminado no puede llamarla.Sin scope, la llamada lee y no escribe registros. Para permitir que use la base de datos , pase un scope, escrito como los permisos de un rol:
Un alcance sólo estrecha: no concede nada que el que llama no tenga ya.Para mostrar la respuesta mientras está escrita, pase onChunk. La llamada todavía resuelve al mismo resultado, una vez que el último fragmento fue entregado a onChunk. Si el final del streamformat@@0 nunca llega, deja de esperar una vez que el resultado está dentro y 5 s pasa sin un nuevo trozo, y sigue entregando cada trozo recibido entonces:
  • La llamada sigue la ejecución a través de la misma suscripción que streamConversation(): se suscribe primero, espera hasta que la suscripción esté en vivo, luego comienza la ejecución, por lo que no pierde ningún chunk.
  • Los trozos son los propios del chat: póngalos en readUIMessageStream() desde el paquete ai para construir el mensaje.
  • signal aborta la llamada: rechaza con el error de aborto y cierra la transmisión . La ejecución en sí misma continúa, y se registra como de costumbre.
  • Una vez que el stream está vivo, un fallo solo detiene los pedazos: la llamada todavía se resuelve con el resultado de la ejecución. que se facturan de cualquier manera. La llamada se rechaza cuando la suscripción falla antes de activarse, por lo que la ejecución nunca comienza, o cuando onChunk genera una excepción.
  • Una aplicación sólo sigue sus propias ejecuciones, así que onChunk necesita un token de aplicación.
  • Requiere "engines": { "twenty": ">=2.56.0" }.
ai.generate() requiere la bandera de permiso de IA en el rol predeterminado de la aplicación, como runAgent().
sendInboxMessage() publica un mensaje de tu aplicación en los chats de uno o más miembros del espacio de trabajo. Puede terminar en una llamada a una herramienta a la que el miembro responde desde el chat, después de lo cual la conversación continúa con el asistente de Twenty. Consulta Inbox para conocer sus opciones, las funciones auxiliares de menciones y cómo publicar desde un componente de front-end.