feat(sdk): add runAgent() to run app agents from logic functions (#21157)

<img width="948" height="593" alt="image"
src="https://github.com/user-attachments/assets/d990fa98-3cfd-469d-ab7f-0b2d4ccf3afc"
/>

<img width="1361" height="802" alt="image"
src="https://github.com/user-attachments/assets/1091f598-49f3-4c16-92ea-1e1c200181e2"
/>


## Add `runAgent()` to the Logic Function SDK

Lets an app's logic function run one of its own AI agents server-side
and get the result back synchronously — reusing the existing agent
executor instead of a new bespoke transport.

  ### Backend
- New **`runAgent` GraphQL mutation** (metadata schema) in
`ai-agent-execution`, wrapping the existing
`AgentAsyncExecutorService.executeAgent`. Scopes the agent lookup to the
calling
  application and runs it under an application auth context.
- New `@AuthApplication()` param decorator (mirrors `@AuthWorkspace()`)
— first GraphQL resolver authenticated by an **application access
token**.
- Guarded by `WorkspaceAuthGuard` +
`SettingsPermissionGuard(PermissionFlagType.AI)`: the app's role must
grant the `AI` permission flag.

  ### SDK
- `runAgent({ agentUniversalIdentifier, prompt })` posts the mutation to
`/metadata` with the app token via a new runtime GraphQL transport.
Returns `{ result, hasNoMoreAvailableCredits
  }`.
- Refactored the connections helpers onto a shared `postAppEndpoint`
util (removes duplicated transport logic).

  ### Frontend
- App install permission modal now shows an explicit consent line —
_"Run AI agents and bill AI credits to your workspace"_ — when the app's
role requests the `AI` flag.

  ### Docs
- Documented `runAgent` and its `AI` permission-flag requirement in
_Skills & Agents_.
- Fixed outdated role-permission examples in _Roles & Permissions_
(`permissionFlags` → `permissionFlagUniversalIdentifiers`,
`PermissionFlag` → `SystemPermissionFlag`).

  ### Test plan
- [x] SDK unit tests (`run-agent.spec.ts`) — request shape, GraphQL/HTTP
error handling, missing env vars
- [x] `twenty-server`, `twenty-front`, `twenty-shared` typecheck + lint
- [ ] Manual: install an app granting the `AI` flag, call `runAgent()`
from a logic function, confirm the agent runs and credits are billed

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
martmull
2026-06-04 18:18:27 +02:00
committed by GitHub
parent 36b654bab3
commit c2ca90c255
38 changed files with 1444 additions and 461 deletions
@@ -2132,6 +2132,18 @@ type FieldConnection {
edges: [FieldEdge!]!
}
type AppConnection {
id: ID!
providerName: String!
name: String!
handle: String!
visibility: String!
userWorkspaceId: String!
accessToken: String!
scopes: [String!]!
authFailedAt: String
}
type ResendEmailVerificationToken {
success: Boolean!
}
@@ -2529,6 +2541,12 @@ type AgentMessagePart {
createdAt: DateTime!
}
type RunAgentResult {
result: JSON
error: String
success: Boolean!
}
type ChannelSyncSuccess {
success: Boolean!
}
@@ -3011,6 +3029,8 @@ type Query {
myMessageChannels(connectedAccountId: UUID): [MessageChannel!]!
myCalendarChannels(connectedAccountId: UUID): [CalendarChannel!]!
minimalMetadata: MinimalMetadata!
appConnections(filter: ListAppConnectionsInput): [AppConnection!]!
appConnection(id: ID!): AppConnection!
findWorkspaceAiStats: WorkspaceAiStats!
chatThreads: [AgentChatThread!]!
chatThread(id: UUID!): AgentChatThread!
@@ -3065,6 +3085,12 @@ input AgentIdInput {
id: UUID!
}
input ListAppConnectionsInput {
providerName: String
userWorkspaceId: String
visibility: String
}
input EventLogQueryInput {
table: EventLogTable!
filters: EventLogFiltersInput
@@ -3238,6 +3264,7 @@ type Mutation {
upsertRowLevelPermissionPredicates(input: UpsertRowLevelPermissionPredicatesInput!): UpsertRowLevelPermissionPredicatesResult!
assignRoleToAgent(agentId: UUID!, roleId: UUID!): Boolean!
removeRoleFromAgent(agentId: UUID!): Boolean!
runAgent(input: RunAgentInput!): RunAgentResult!
createWebhook(input: CreateWebhookInput!): Webhook!
updateWebhook(input: UpdateWebhookInput!): Webhook!
deleteWebhook(id: UUID!): Webhook!
@@ -4093,6 +4120,11 @@ input RowLevelPermissionPredicateGroupInput {
positionInRowLevelPermissionPredicateGroup: Float
}
input RunAgentInput {
agentUniversalIdentifier: String!
prompt: String!
}
input CreateWebhookInput {
id: UUID
targetUrl: String!
@@ -1762,6 +1762,19 @@ export interface FieldConnection {
__typename: 'FieldConnection'
}
export interface AppConnection {
id: Scalars['ID']
providerName: Scalars['String']
name: Scalars['String']
handle: Scalars['String']
visibility: Scalars['String']
userWorkspaceId: Scalars['String']
accessToken: Scalars['String']
scopes: Scalars['String'][]
authFailedAt?: Scalars['String']
__typename: 'AppConnection'
}
export interface ResendEmailVerificationToken {
success: Scalars['Boolean']
__typename: 'ResendEmailVerificationToken'
@@ -2211,6 +2224,13 @@ export interface AgentMessagePart {
__typename: 'AgentMessagePart'
}
export interface RunAgentResult {
result?: Scalars['JSON']
error?: Scalars['String']
success: Scalars['Boolean']
__typename: 'RunAgentResult'
}
export interface ChannelSyncSuccess {
success: Scalars['Boolean']
__typename: 'ChannelSyncSuccess'
@@ -2607,6 +2627,8 @@ export interface Query {
myMessageChannels: MessageChannel[]
myCalendarChannels: CalendarChannel[]
minimalMetadata: MinimalMetadata
appConnections: AppConnection[]
appConnection: AppConnection
findWorkspaceAiStats: WorkspaceAiStats
chatThreads: AgentChatThread[]
chatThread: AgentChatThread
@@ -2767,6 +2789,7 @@ export interface Mutation {
upsertRowLevelPermissionPredicates: UpsertRowLevelPermissionPredicatesResult
assignRoleToAgent: Scalars['Boolean']
removeRoleFromAgent: Scalars['Boolean']
runAgent: RunAgentResult
createWebhook: Webhook
updateWebhook: Webhook
deleteWebhook: Webhook
@@ -4722,6 +4745,20 @@ export interface FieldConnectionGenqlSelection{
__scalar?: boolean | number
}
export interface AppConnectionGenqlSelection{
id?: boolean | number
providerName?: boolean | number
name?: boolean | number
handle?: boolean | number
visibility?: boolean | number
userWorkspaceId?: boolean | number
accessToken?: boolean | number
scopes?: boolean | number
authFailedAt?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface ResendEmailVerificationTokenGenqlSelection{
success?: boolean | number
__typename?: boolean | number
@@ -5226,6 +5263,14 @@ export interface AgentMessagePartGenqlSelection{
__scalar?: boolean | number
}
export interface RunAgentResultGenqlSelection{
result?: boolean | number
error?: boolean | number
success?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface ChannelSyncSuccessGenqlSelection{
success?: boolean | number
__typename?: boolean | number
@@ -5646,6 +5691,8 @@ export interface QueryGenqlSelection{
myMessageChannels?: (MessageChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} })
myCalendarChannels?: (CalendarChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} })
minimalMetadata?: MinimalMetadataGenqlSelection
appConnections?: (AppConnectionGenqlSelection & { __args?: {filter?: (ListAppConnectionsInput | null)} })
appConnection?: (AppConnectionGenqlSelection & { __args: {id: Scalars['ID']} })
findWorkspaceAiStats?: WorkspaceAiStatsGenqlSelection
chatThreads?: AgentChatThreadGenqlSelection
chatThread?: (AgentChatThreadGenqlSelection & { __args: {id: Scalars['UUID']} })
@@ -5698,6 +5745,8 @@ export interface AgentIdInput {
/** The id of the agent. */
id: Scalars['UUID']}
export interface ListAppConnectionsInput {providerName?: (Scalars['String'] | null),userWorkspaceId?: (Scalars['String'] | null),visibility?: (Scalars['String'] | null)}
export interface EventLogQueryInput {table: EventLogTable,filters?: (EventLogFiltersInput | null),first?: (Scalars['Int'] | null),after?: (Scalars['String'] | null)}
export interface EventLogFiltersInput {eventType?: (Scalars['String'] | null),userWorkspaceId?: (Scalars['String'] | null),dateRange?: (EventLogDateRangeInput | null),recordId?: (Scalars['String'] | null),objectMetadataId?: (Scalars['String'] | null)}
@@ -5827,6 +5876,7 @@ export interface MutationGenqlSelection{
upsertRowLevelPermissionPredicates?: (UpsertRowLevelPermissionPredicatesResultGenqlSelection & { __args: {input: UpsertRowLevelPermissionPredicatesInput} })
assignRoleToAgent?: { __args: {agentId: Scalars['UUID'], roleId: Scalars['UUID']} }
removeRoleFromAgent?: { __args: {agentId: Scalars['UUID']} }
runAgent?: (RunAgentResultGenqlSelection & { __args: {input: RunAgentInput} })
createWebhook?: (WebhookGenqlSelection & { __args: {input: CreateWebhookInput} })
updateWebhook?: (WebhookGenqlSelection & { __args: {input: UpdateWebhookInput} })
deleteWebhook?: (WebhookGenqlSelection & { __args: {id: Scalars['UUID']} })
@@ -6184,6 +6234,8 @@ export interface RowLevelPermissionPredicateInput {id?: (Scalars['UUID'] | null)
export interface RowLevelPermissionPredicateGroupInput {id?: (Scalars['UUID'] | null),objectMetadataId: Scalars['UUID'],parentRowLevelPermissionPredicateGroupId?: (Scalars['UUID'] | null),logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator,positionInRowLevelPermissionPredicateGroup?: (Scalars['Float'] | null)}
export interface RunAgentInput {agentUniversalIdentifier: Scalars['String'],prompt: Scalars['String']}
export interface CreateWebhookInput {id?: (Scalars['UUID'] | null),targetUrl: Scalars['String'],operations: Scalars['String'][],description?: (Scalars['String'] | null),secret?: (Scalars['String'] | null)}
export interface UpdateWebhookInput {
@@ -7535,6 +7587,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const AppConnection_possibleTypes: string[] = ['AppConnection']
export const isAppConnection = (obj?: { __typename?: any } | null): obj is AppConnection => {
if (!obj?.__typename) throw new Error('__typename is missing in "isAppConnection"')
return AppConnection_possibleTypes.includes(obj.__typename)
}
const ResendEmailVerificationToken_possibleTypes: string[] = ['ResendEmailVerificationToken']
export const isResendEmailVerificationToken = (obj?: { __typename?: any } | null): obj is ResendEmailVerificationToken => {
if (!obj?.__typename) throw new Error('__typename is missing in "isResendEmailVerificationToken"')
@@ -7991,6 +8051,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const RunAgentResult_possibleTypes: string[] = ['RunAgentResult']
export const isRunAgentResult = (obj?: { __typename?: any } | null): obj is RunAgentResult => {
if (!obj?.__typename) throw new Error('__typename is missing in "isRunAgentResult"')
return RunAgentResult_possibleTypes.includes(obj.__typename)
}
const ChannelSyncSuccess_possibleTypes: string[] = ['ChannelSyncSuccess']
export const isChannelSyncSuccess = (obj?: { __typename?: any } | null): obj is ChannelSyncSuccess => {
if (!obj?.__typename) throw new Error('__typename is missing in "isChannelSyncSuccess"')
File diff suppressed because it is too large Load Diff