defineSkill
Define AI agent skills
defineSkill
Define AI agent skills
Skills define reusable instructions and capabilities that AI agents can use within your workspace. Use Key points:
defineSkill() to define skills with built-in validation:src/skills/example-skill.ts
nameis a unique identifier string for the skill (kebab-case recommended).labelis the human-readable display name shown in the UI.contentcontains the skill instructions — this is the text the AI agent uses.icon(optional) sets the icon displayed in the UI.description(optional) provides additional context about the skill’s purpose.
defineAgent
Define AI agents with custom prompts
defineAgent
Define AI agents with custom prompts
Agents are AI assistants that live inside your workspace. Use Key points:Schema notes:
defineAgent() to create agents with a custom system prompt:src/agents/example-agent.ts
nameis the unique identifier string for the agent (kebab-case recommended).labelis the display name shown in the UI.promptis the system prompt that defines the agent’s behavior.description(optional) provides context about what the agent does.icon(optional) sets the icon displayed in the UI.modelId(optional) overrides the default AI model used by the agent.responseFormat(optional) sets the agent’s default output shape. Defaults to{ type: 'text' }for free-form text. Use{ type: 'json', schema }to force structured JSON output. ArunAgent()call or a workflow AI step can ask for a different shape, which takes precedence.roleUniversalIdentifier(optional) binds the agent to a role your app defines. That role must not grant more than the application role, or the build fails; see Agent roles.
responseFormat to { type: 'json' } and provide a schema:src/agents/structured-agent.ts
- The schema is a flat object: each property’s
typemust be a primitive (string,number, orboolean). Nested objects and arrays are not supported. description(optional) on each property guides the model on what to put there.required(optional) lists the properties the model must always return.additionalProperties: false(optional) forbids any property not declared inproperties.
runAgent
Run an agent from a logic function
runAgent
Run an agent from a logic function
runAgent() lets a logic function run one of your app’s agents (with its
skills and tools). Identify the agent by the universalIdentifier you passed
to defineAgent(), and pass what you want it to work on as input:src/logic-functions/run-enricher.ts
inputis a string, or a list of 1 to 100 messages ({ role: 'user' | 'assistant', content: string }). Ausermessage can also carry file attachments (see below). To hold a conversation, pass athreadrather than replaying earlier turns yourself.- By default the call waits for the agent, which can read/update records
itself via its own tools —
runAgent()resolves once the run completes. PasswaitForCompletion: falseto resolve as soon as the run starts:statusis thenRUNNINGandresult: null, and the reply lands in thethreadIdconversation. A run that outlasts 15 minutes also resolves asRUNNING. - The agent can also pause until a record changes or some time has passed.
runAgent()then resolves withsuccess: true,status: 'SUSPENDED'andresult: null; the agent goes on by itself later, and its reply lands in thethreadIdconversation. A call to athreadwhose agent is still waiting fails until it finishes. - Every call is stored with its input and status, plus its reply and usage
when it completes, so it shows up in the agent’s runs. Without a
thread, a call gets a conversation of its own:threadIdnames it, and a later call never sees it. - An app can only run its own agents.
- The app’s default role must grant the
AIpermission flag — addSystemPermissionFlag.AIto itspermissionFlagUniversalIdentifiers(or setcanAccessAllTools: true). Without it,runAgent()fails with a permission error. - Set a generous
timeoutSecondson the logic function — agent runs can take several seconds. statusisCOMPLETEDwith a non-nullresultwhen the run completes without waiting;SUSPENDEDwithsuccess: trueandresult: nullwhile it waits;RUNNINGwithsuccess: trueandresult: nullwhen the call did not wait for the run;FAILEDwithsuccess: falseanderrorholding the reason (for example, when the workspace ran out of AI credits mid-run).
src/roles/default-role.ts
Sending files with a message
Auser message can carry attachments, so the agent sees a screenshot or a
PDF instead of only its name. Each entry references a file your app has already
uploaded into the agent-chat folder:src/logic-functions/describe-screenshot.ts
UPLOAD_FILE permission flag on the app’s default role,
alongside AI. filename is optional and only labels the file for the model.- Only
usermessages may carry attachments. They cannot be represented on an assistant turn, sorunAgent()rejects them there rather than dropping them. - At most 10 attachments per message.
- The
fileIdmust name a fully uploaded file in the same workspace, under theagent-chatfolder. Anything else fails the call instead of quietly answering without the file.
Asking for a structured result
PassresponseFormat to get a shape for this call only. It takes precedence over
the agent’s own responseFormat, so one agent can serve callers that need
different outputs. It uses the same schema as defineAgent():src/logic-functions/score-lead.ts
Continuing a conversation
Pass athread and Twenty keeps the conversation for you: each call sends only
the new input, and the agent sees the earlier turns, including what its tools
returned. Use any stable identifier from your side as the key, such as a Slack
channel and thread timestamp:src/logic-functions/reply-in-thread.ts
- Keys are scoped to your app and agent, so two apps never share a
conversation. Only app access tokens can pass a
thread. inputmay only holdusermessages: the conversation already holds the agent’s replies.additionalInstructionsare given to the model for this call only and are not stored, which suits per-request guidance such as formatting rules or a deadline. They work without athreadtoo.- Every run is stored as a turn with its status, failed ones included. Its usage is stored when the agent returns, even if it ran out of credits. A failed run is not part of the conversation, so the next call does not see it.
- Calls on the same thread run one at a time: a second call waits for the first to finish, so each one sees the turns before it.
- When turns run as different members (see below), each one only sees the text of the others’ turns, never their tool results, since those were read with someone else’s permissions.
Running on behalf of a workspace member
PassrunAsWorkspaceMemberId when the run is triggered by a person — a chat
bot answering a message, for instance — so the agent acts as that member
instead of as the app:src/logic-functions/answer-question.ts
- A token with no user attached may name any member. A logic function runs
with one, and so do tokens minted through
client_credentialsor from an API key. - A token issued on behalf of a user, as a front component receives, may only name that user’s own member.
sendInboxMessage
Start or continue a conversation in members' chats
sendInboxMessage
Start or continue a conversation in members' chats
sendInboxMessage() posts a message from your app in the chats of one or more
workspace members. It can end on a tool call the member answers from the chat, after which
the conversation continues with Twenty’s assistant.src/logic-functions/notify-first-recording.ts
workspaceMemberIdslists the members who receive the conversation, at least one. The first member of a new conversation owns it, and the others follow it as participants. A later send under the same key that lists new members adds them; members it leaves out keep the conversation.threadKeypicks the conversation for this app, shared by every member it is sent to. A new key starts a conversation titledtitle; a key you used before adds the message to that conversation.idempotencyKeyidentifies the message within the conversation. Sending again with the same keys writes nothing new, so retries and concurrent runs never duplicate a message.toolCall(optional) ends the message on one of:- a call the member answers, which pauses the conversation:
ask_question(one multiple-choice question),request_form(fields to fill in) orpropose_tool_call(a call to approve). Itsinputtakes thetoolName, theargumentsyou would call the tool with and a one-sentencesummary. The tool must be one your app’s default role could run itself, such ascreate_one_personorupdate_one_opportunity(with the recordid); an email (send_emailordraft_email, withrecipients,subjectand abodyin HTML) needs no permission of your app. The member reviews the call in the same card as a call the assistant proposes, edits it if they want, and the approved call runs with their own access; - one of your app’s own tools, by
logicFunctionUniversalIdentifier, rendered by its front component with theinputandoutputyou pass. It does not pause the conversation.
- a call the member answers, which pauses the conversation:
- Only one call the member answers can wait in a conversation at a time:
sending another before they answer fails with
THREAD_AWAITING_ANSWER. Plain messages and app tool calls are still accepted. - Only the member who owns a conversation can delete it, and a deleted conversation is gone for every member: it is not recreated, and nothing is added to it. The other members can unsubscribe to take it out of their own inbox.
- Every member must have the
AIpermission, and the call fails without writing the message when one of them cannot have the conversation. Your app’s default role must grantSystemPermissionFlag.AI.