feat(workflow): add update_agent tool and responseFormat-aware AI Agent step schema (#21755)

## Summary

Brings the workflow AI/MCP tooling for **AI Agent steps** to parity with
the existing **CODE / logic-function** flow, and makes AI Agent output
references reliable in validation and the variable picker.

Just like a CODE step needs a logic function, an `AI_AGENT` step needs
an agent. The agent is already created as a side effect of step
creation; this PR adds the missing "configure it" tooling and fixes the
output schema so it reflects the agent's actual response format.

## Changes

### New `update_agent` MCP tool
- `update-agent.tool.ts`: lets the assistant configure the agent backing
an `AI_AGENT` step — `prompt` (system prompt), optional `modelId`,
optional `responseFormat` (text or structured json) — via
`AgentService.updateOneAgent`. Direct analog of
`update_logic_function_source`.
- Wired through: added `agentService` to `WorkflowToolDependencies`,
injected `AgentService` and registered the tool in
`workflow-tool.workspace-service.ts`, imported `AiAgentModule` in
`workflow-tools.module.ts`.

### Guided creation flow (mirrors CODE)
- `create-workflow-version-step.tool.ts`: `enrichResultWithNextStep` now
returns an `AI_AGENT` hint instructing the assistant to call
`update_agent` with the step's `settings.input.agentId` (and to set the
task prompt via `update_workflow_version_step` if needed).
- `create-complete-workflow.tool.ts`: rejects `AI_AGENT` steps (it
inserts steps directly and never runs the side effect that creates the
agent), with a description note pointing to
`create_workflow_version_step` + `update_agent`. Same treatment CODE
already gets.

### Correct output schema for AI Agent steps
- Backend `computeStepOutputSchema`
(`workflow-schema.workspace-service.ts`): the `AI_AGENT` case now
derives the output schema from the agent's `responseFormat` instead of a
hardcoded `{ response }`:
  - text → `{ response: string }`
  - json → one leaf per `responseFormat.schema.properties` field
This makes workflow validation resolve `{{stepId.fieldName}}` references
against the agent's real output (previously json agents validated wrong:
real fields rejected, `{{stepId.response}}` accepted but undefined at
runtime).
- Frontend `useStepsOutputSchema.ts`: when an `AI_AGENT` step has no
persisted `outputSchema`, fall back to generating it from the agent's
`responseFormat` (via `FindManyAgents` + the existing
`agentResponseSchemaToOutputSchema`) instead of the hardcoded `{
response }`. Keeps the variable picker correct for structured agents.

## Why output schema matters

Validation resolves every `{{stepId.path}}` against the referenced
step's `settings.outputSchema` (`validateWorkflowVariableReferences`).
The agent step's output schema is therefore the single source of truth
for "is the right output referenced." Because the runtime output depends
on `responseFormat` (text → `{ response }`, json → schema fields
directly), the schema must be derived from `responseFormat` to be
accurate.

## Not solved issue
We want each AI_AGENT step's settings.outputSchema to always match the
backing agent's responseFormat:
- text → { response }
- json → one field per responseFormat.schema.properties
That output schema is what everything downstream relies on: validation
(validateWorkflowVariableReferences resolves {{stepId.field}} against
it), the frontend variable picker (useStepsOutputSchema), and it's also
persisted inside workflowVersion.steps.

Agent should be unique source of truth but syncing agent -> step is not
possible


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21755?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Etienne
2026-06-18 15:00:21 +02:00
committed by GitHub
parent 3fe2aec5d1
commit d67aa2889b
10 changed files with 751 additions and 564 deletions
@@ -3046,14 +3046,8 @@ type Query {
getPageLayout(id: String!): PageLayout
getPageLayoutWidgets(pageLayoutTabId: String!): [PageLayoutWidget!]!
getPageLayoutWidget(id: String!): PageLayoutWidget!
findOneLogicFunction(input: LogicFunctionIdInput!): LogicFunction!
findManyLogicFunctions: [LogicFunction!]!
getAvailablePackages(input: LogicFunctionIdInput!): JSON!
getLogicFunctionSourceCode(input: LogicFunctionIdInput!): String
commandMenuItems: [CommandMenuItem!]!
commandMenuItem(id: UUID!): CommandMenuItem
frontComponents: [FrontComponent!]!
frontComponent(id: UUID!): FrontComponent
findManyAgents: [Agent!]!
findOneAgent(input: AgentIdInput!): Agent!
objectRecordCounts: [ObjectRecordCount!]!
object(
"""The id of the record to find."""
@@ -3077,8 +3071,14 @@ type Query {
"""Specify to filter the records returned."""
filter: IndexFilter! = {}
): IndexConnection!
findManyAgents: [Agent!]!
findOneAgent(input: AgentIdInput!): Agent!
findOneLogicFunction(input: LogicFunctionIdInput!): LogicFunction!
findManyLogicFunctions: [LogicFunction!]!
getAvailablePackages(input: LogicFunctionIdInput!): JSON!
getLogicFunctionSourceCode(input: LogicFunctionIdInput!): String
commandMenuItems: [CommandMenuItem!]!
commandMenuItem(id: UUID!): CommandMenuItem
frontComponents: [FrontComponent!]!
frontComponent(id: UUID!): FrontComponent
previewMessageCampaignAudience(input: PreviewMessageCampaignAudienceInput!): CampaignAudiencePreviewDTO!
unsubscribeTopics: [UnsubscribeTopic!]!
unsubscribePagePreviewUrl: String!
@@ -3154,16 +3154,16 @@ input GetApiKeyInput {
id: UUID!
}
input LogicFunctionIdInput {
"""The id of the function."""
id: ID!
}
input AgentIdInput {
"""The id of the agent."""
id: UUID!
}
input LogicFunctionIdInput {
"""The id of the function."""
id: ID!
}
input PreviewMessageCampaignAudienceInput {
listId: String!
unsubscribeTopicId: String
@@ -3314,6 +3314,14 @@ type Mutation {
createPageLayoutWidget(input: CreatePageLayoutWidgetInput!): PageLayoutWidget!
updatePageLayoutWidget(id: String!, input: UpdatePageLayoutWidgetInput!): PageLayoutWidget!
destroyPageLayoutWidget(id: String!): Boolean!
createOneAgent(input: CreateAgentInput!): Agent!
updateOneAgent(input: UpdateAgentInput!): Agent!
deleteOneAgent(input: AgentIdInput!): Agent!
createOneObject(input: CreateOneObjectInput!): Object!
deleteOneObject(input: DeleteOneObjectInput!): Object!
updateOneObject(input: UpdateOneObjectInput!): Object!
createOneIndex(input: CreateOneIndexInput!): Index!
deleteOneIndex(input: DeleteOneIndexInput!): Index!
deleteOneLogicFunction(input: LogicFunctionIdInput!): LogicFunction!
createOneLogicFunction(input: CreateLogicFunctionFromSourceInput!): LogicFunction!
executeOneLogicFunction(input: ExecuteOneLogicFunctionInput!): LogicFunctionExecutionResult!
@@ -3325,14 +3333,6 @@ type Mutation {
createFrontComponent(input: CreateFrontComponentInput!): FrontComponent!
updateFrontComponent(input: UpdateFrontComponentInput!): FrontComponent!
deleteFrontComponent(id: UUID!): FrontComponent!
createOneObject(input: CreateOneObjectInput!): Object!
deleteOneObject(input: DeleteOneObjectInput!): Object!
updateOneObject(input: UpdateOneObjectInput!): Object!
createOneIndex(input: CreateOneIndexInput!): Index!
deleteOneIndex(input: DeleteOneIndexInput!): Index!
createOneAgent(input: CreateAgentInput!): Agent!
updateOneAgent(input: UpdateAgentInput!): Agent!
deleteOneAgent(input: AgentIdInput!): Agent!
sendEmailViaEmailingDomain(input: SendEmailViaDomainInput!): SendEmailViaDomainOutput!
sendMessageCampaign(input: SendMessageCampaignInput!): SendMessageCampaignOutputDTO!
createUnsubscribeTopic(input: CreateUnsubscribeTopicInput!): UnsubscribeTopic!
@@ -3903,6 +3903,103 @@ input UpdatePageLayoutWidgetInput {
conditionalAvailabilityExpression: String
}
input CreateAgentInput {
name: String
label: String!
icon: String
description: String
prompt: String!
modelId: String!
roleId: UUID
responseFormat: JSON
modelConfiguration: JSON
evaluationInputs: [String!]
}
input UpdateAgentInput {
id: UUID!
name: String
label: String
icon: String
description: String
prompt: String
modelId: String
roleId: UUID
responseFormat: JSON
modelConfiguration: JSON
evaluationInputs: [String!]
}
input CreateOneObjectInput {
"""The object to create"""
object: CreateObjectInput!
}
input CreateObjectInput {
nameSingular: String!
namePlural: String!
labelSingular: String!
labelPlural: String!
description: String
icon: String
shortcut: String
color: String
skipNameField: Boolean
isRemote: Boolean
primaryKeyColumnType: String
primaryKeyFieldMetadataSettings: JSON
isLabelSyncedWithName: Boolean
}
input DeleteOneObjectInput {
"""The id of the record to delete."""
id: UUID!
}
input UpdateOneObjectInput {
update: UpdateObjectPayload!
"""The id of the object to update"""
id: UUID!
}
input UpdateObjectPayload {
labelSingular: String
labelPlural: String
nameSingular: String
namePlural: String
description: String
icon: String
shortcut: String
color: String
isActive: Boolean
labelIdentifierFieldMetadataId: UUID
imageIdentifierFieldMetadataId: UUID
isLabelSyncedWithName: Boolean
isSearchable: Boolean
}
input CreateOneIndexInput {
"""The custom index to create"""
index: CreateIndexInput!
}
input CreateIndexInput {
objectMetadataId: UUID!
fields: [CreateIndexFieldInput!]!
indexType: IndexType! = BTREE
}
input CreateIndexFieldInput {
fieldMetadataId: UUID!
subFieldName: String
}
input DeleteOneIndexInput {
"""The id of the custom index to delete."""
id: UUID!
}
input CreateLogicFunctionFromSourceInput {
id: UUID
universalIdentifier: UUID
@@ -4001,103 +4098,6 @@ input UpdateFrontComponentInputUpdates {
description: String
}
input CreateOneObjectInput {
"""The object to create"""
object: CreateObjectInput!
}
input CreateObjectInput {
nameSingular: String!
namePlural: String!
labelSingular: String!
labelPlural: String!
description: String
icon: String
shortcut: String
color: String
skipNameField: Boolean
isRemote: Boolean
primaryKeyColumnType: String
primaryKeyFieldMetadataSettings: JSON
isLabelSyncedWithName: Boolean
}
input DeleteOneObjectInput {
"""The id of the record to delete."""
id: UUID!
}
input UpdateOneObjectInput {
update: UpdateObjectPayload!
"""The id of the object to update"""
id: UUID!
}
input UpdateObjectPayload {
labelSingular: String
labelPlural: String
nameSingular: String
namePlural: String
description: String
icon: String
shortcut: String
color: String
isActive: Boolean
labelIdentifierFieldMetadataId: UUID
imageIdentifierFieldMetadataId: UUID
isLabelSyncedWithName: Boolean
isSearchable: Boolean
}
input CreateOneIndexInput {
"""The custom index to create"""
index: CreateIndexInput!
}
input CreateIndexInput {
objectMetadataId: UUID!
fields: [CreateIndexFieldInput!]!
indexType: IndexType! = BTREE
}
input CreateIndexFieldInput {
fieldMetadataId: UUID!
subFieldName: String
}
input DeleteOneIndexInput {
"""The id of the custom index to delete."""
id: UUID!
}
input CreateAgentInput {
name: String
label: String!
icon: String
description: String
prompt: String!
modelId: String!
roleId: UUID
responseFormat: JSON
modelConfiguration: JSON
evaluationInputs: [String!]
}
input UpdateAgentInput {
id: UUID!
name: String
label: String
icon: String
description: String
prompt: String
modelId: String
roleId: UUID
responseFormat: JSON
modelConfiguration: JSON
evaluationInputs: [String!]
}
input SendEmailViaDomainInput {
emailingDomainId: String!
to: [String!]!
@@ -2673,6 +2673,13 @@ export interface Query {
getPageLayout?: PageLayout
getPageLayoutWidgets: PageLayoutWidget[]
getPageLayoutWidget: PageLayoutWidget
findManyAgents: Agent[]
findOneAgent: Agent
objectRecordCounts: ObjectRecordCount[]
object: Object
objects: ObjectConnection
index: Index
indexMetadatas: IndexConnection
findOneLogicFunction: LogicFunction
findManyLogicFunctions: LogicFunction[]
getAvailablePackages: Scalars['JSON']
@@ -2681,13 +2688,6 @@ export interface Query {
commandMenuItem?: CommandMenuItem
frontComponents: FrontComponent[]
frontComponent?: FrontComponent
objectRecordCounts: ObjectRecordCount[]
object: Object
objects: ObjectConnection
index: Index
indexMetadatas: IndexConnection
findManyAgents: Agent[]
findOneAgent: Agent
previewMessageCampaignAudience: CampaignAudiencePreviewDTO
unsubscribeTopics: UnsubscribeTopic[]
unsubscribePagePreviewUrl: Scalars['String']
@@ -2835,6 +2835,14 @@ export interface Mutation {
createPageLayoutWidget: PageLayoutWidget
updatePageLayoutWidget: PageLayoutWidget
destroyPageLayoutWidget: Scalars['Boolean']
createOneAgent: Agent
updateOneAgent: Agent
deleteOneAgent: Agent
createOneObject: Object
deleteOneObject: Object
updateOneObject: Object
createOneIndex: Index
deleteOneIndex: Index
deleteOneLogicFunction: LogicFunction
createOneLogicFunction: LogicFunction
executeOneLogicFunction: LogicFunctionExecutionResult
@@ -2846,14 +2854,6 @@ export interface Mutation {
createFrontComponent: FrontComponent
updateFrontComponent: FrontComponent
deleteFrontComponent: FrontComponent
createOneObject: Object
deleteOneObject: Object
updateOneObject: Object
createOneIndex: Index
deleteOneIndex: Index
createOneAgent: Agent
updateOneAgent: Agent
deleteOneAgent: Agent
sendEmailViaEmailingDomain: SendEmailViaDomainOutput
sendMessageCampaign: SendMessageCampaignOutputDTO
createUnsubscribeTopic: UnsubscribeTopic
@@ -5808,14 +5808,8 @@ export interface QueryGenqlSelection{
getPageLayout?: (PageLayoutGenqlSelection & { __args: {id: Scalars['String']} })
getPageLayoutWidgets?: (PageLayoutWidgetGenqlSelection & { __args: {pageLayoutTabId: Scalars['String']} })
getPageLayoutWidget?: (PageLayoutWidgetGenqlSelection & { __args: {id: Scalars['String']} })
findOneLogicFunction?: (LogicFunctionGenqlSelection & { __args: {input: LogicFunctionIdInput} })
findManyLogicFunctions?: LogicFunctionGenqlSelection
getAvailablePackages?: { __args: {input: LogicFunctionIdInput} }
getLogicFunctionSourceCode?: { __args: {input: LogicFunctionIdInput} }
commandMenuItems?: CommandMenuItemGenqlSelection
commandMenuItem?: (CommandMenuItemGenqlSelection & { __args: {id: Scalars['UUID']} })
frontComponents?: FrontComponentGenqlSelection
frontComponent?: (FrontComponentGenqlSelection & { __args: {id: Scalars['UUID']} })
findManyAgents?: AgentGenqlSelection
findOneAgent?: (AgentGenqlSelection & { __args: {input: AgentIdInput} })
objectRecordCounts?: ObjectRecordCountGenqlSelection
object?: (ObjectGenqlSelection & { __args: {
/** The id of the record to find. */
@@ -5833,8 +5827,14 @@ export interface QueryGenqlSelection{
paging: CursorPaging,
/** Specify to filter the records returned. */
filter: IndexFilter} })
findManyAgents?: AgentGenqlSelection
findOneAgent?: (AgentGenqlSelection & { __args: {input: AgentIdInput} })
findOneLogicFunction?: (LogicFunctionGenqlSelection & { __args: {input: LogicFunctionIdInput} })
findManyLogicFunctions?: LogicFunctionGenqlSelection
getAvailablePackages?: { __args: {input: LogicFunctionIdInput} }
getLogicFunctionSourceCode?: { __args: {input: LogicFunctionIdInput} }
commandMenuItems?: CommandMenuItemGenqlSelection
commandMenuItem?: (CommandMenuItemGenqlSelection & { __args: {id: Scalars['UUID']} })
frontComponents?: FrontComponentGenqlSelection
frontComponent?: (FrontComponentGenqlSelection & { __args: {id: Scalars['UUID']} })
previewMessageCampaignAudience?: (CampaignAudiencePreviewDTOGenqlSelection & { __args: {input: PreviewMessageCampaignAudienceInput} })
unsubscribeTopics?: UnsubscribeTopicGenqlSelection
unsubscribePagePreviewUrl?: boolean | number
@@ -5907,14 +5907,14 @@ export interface QueryGenqlSelection{
export interface GetApiKeyInput {id: Scalars['UUID']}
export interface LogicFunctionIdInput {
/** The id of the function. */
id: Scalars['ID']}
export interface AgentIdInput {
/** The id of the agent. */
id: Scalars['UUID']}
export interface LogicFunctionIdInput {
/** The id of the function. */
id: Scalars['ID']}
export interface PreviewMessageCampaignAudienceInput {listId: Scalars['String'],unsubscribeTopicId?: (Scalars['String'] | null)}
export interface ListAppConnectionsInput {providerName?: (Scalars['String'] | null),userWorkspaceId?: (Scalars['String'] | null),visibility?: (Scalars['String'] | null)}
@@ -6013,6 +6013,14 @@ export interface MutationGenqlSelection{
createPageLayoutWidget?: (PageLayoutWidgetGenqlSelection & { __args: {input: CreatePageLayoutWidgetInput} })
updatePageLayoutWidget?: (PageLayoutWidgetGenqlSelection & { __args: {id: Scalars['String'], input: UpdatePageLayoutWidgetInput} })
destroyPageLayoutWidget?: { __args: {id: Scalars['String']} }
createOneAgent?: (AgentGenqlSelection & { __args: {input: CreateAgentInput} })
updateOneAgent?: (AgentGenqlSelection & { __args: {input: UpdateAgentInput} })
deleteOneAgent?: (AgentGenqlSelection & { __args: {input: AgentIdInput} })
createOneObject?: (ObjectGenqlSelection & { __args: {input: CreateOneObjectInput} })
deleteOneObject?: (ObjectGenqlSelection & { __args: {input: DeleteOneObjectInput} })
updateOneObject?: (ObjectGenqlSelection & { __args: {input: UpdateOneObjectInput} })
createOneIndex?: (IndexGenqlSelection & { __args: {input: CreateOneIndexInput} })
deleteOneIndex?: (IndexGenqlSelection & { __args: {input: DeleteOneIndexInput} })
deleteOneLogicFunction?: (LogicFunctionGenqlSelection & { __args: {input: LogicFunctionIdInput} })
createOneLogicFunction?: (LogicFunctionGenqlSelection & { __args: {input: CreateLogicFunctionFromSourceInput} })
executeOneLogicFunction?: (LogicFunctionExecutionResultGenqlSelection & { __args: {input: ExecuteOneLogicFunctionInput} })
@@ -6024,14 +6032,6 @@ export interface MutationGenqlSelection{
createFrontComponent?: (FrontComponentGenqlSelection & { __args: {input: CreateFrontComponentInput} })
updateFrontComponent?: (FrontComponentGenqlSelection & { __args: {input: UpdateFrontComponentInput} })
deleteFrontComponent?: (FrontComponentGenqlSelection & { __args: {id: Scalars['UUID']} })
createOneObject?: (ObjectGenqlSelection & { __args: {input: CreateOneObjectInput} })
deleteOneObject?: (ObjectGenqlSelection & { __args: {input: DeleteOneObjectInput} })
updateOneObject?: (ObjectGenqlSelection & { __args: {input: UpdateOneObjectInput} })
createOneIndex?: (IndexGenqlSelection & { __args: {input: CreateOneIndexInput} })
deleteOneIndex?: (IndexGenqlSelection & { __args: {input: DeleteOneIndexInput} })
createOneAgent?: (AgentGenqlSelection & { __args: {input: CreateAgentInput} })
updateOneAgent?: (AgentGenqlSelection & { __args: {input: UpdateAgentInput} })
deleteOneAgent?: (AgentGenqlSelection & { __args: {input: AgentIdInput} })
sendEmailViaEmailingDomain?: (SendEmailViaDomainOutputGenqlSelection & { __args: {input: SendEmailViaDomainInput} })
sendMessageCampaign?: (SendMessageCampaignOutputDTOGenqlSelection & { __args: {input: SendMessageCampaignInput} })
createUnsubscribeTopic?: (UnsubscribeTopicGenqlSelection & { __args: {input: CreateUnsubscribeTopicInput} })
@@ -6323,35 +6323,9 @@ export interface CreatePageLayoutWidgetInput {pageLayoutTabId: Scalars['UUID'],t
export interface UpdatePageLayoutWidgetInput {pageLayoutTabId?: (Scalars['UUID'] | null),title?: (Scalars['String'] | null),type?: (WidgetType | null),objectMetadataId?: (Scalars['UUID'] | null),gridPosition?: (GridPositionInput | null),position?: (Scalars['JSON'] | null),configuration?: (Scalars['JSON'] | null),conditionalDisplay?: (Scalars['JSON'] | null),conditionalAvailabilityExpression?: (Scalars['String'] | null)}
export interface CreateLogicFunctionFromSourceInput {id?: (Scalars['UUID'] | null),universalIdentifier?: (Scalars['UUID'] | null),name: Scalars['String'],description?: (Scalars['String'] | null),timeoutSeconds?: (Scalars['Float'] | null),source?: (Scalars['JSON'] | null),cronTriggerSettings?: (Scalars['JSON'] | null),databaseEventTriggerSettings?: (Scalars['JSON'] | null),httpRouteTriggerSettings?: (Scalars['JSON'] | null),toolTriggerSettings?: (Scalars['JSON'] | null),workflowActionTriggerSettings?: (Scalars['JSON'] | null)}
export interface CreateAgentInput {name?: (Scalars['String'] | null),label: Scalars['String'],icon?: (Scalars['String'] | null),description?: (Scalars['String'] | null),prompt: Scalars['String'],modelId: Scalars['String'],roleId?: (Scalars['UUID'] | null),responseFormat?: (Scalars['JSON'] | null),modelConfiguration?: (Scalars['JSON'] | null),evaluationInputs?: (Scalars['String'][] | null)}
export interface ExecuteOneLogicFunctionInput {
/** Id of the logic function to execute */
id: Scalars['UUID'],
/** Payload in JSON format */
payload: Scalars['JSON']}
export interface UpdateLogicFunctionFromSourceInput {
/** Id of the logic function to update */
id: Scalars['UUID'],
/** The logic function updates */
update: UpdateLogicFunctionFromSourceInputUpdates}
export interface UpdateLogicFunctionFromSourceInputUpdates {name?: (Scalars['String'] | null),description?: (Scalars['String'] | null),timeoutSeconds?: (Scalars['Float'] | null),sourceHandlerCode?: (Scalars['String'] | null),handlerName?: (Scalars['String'] | null),sourceHandlerPath?: (Scalars['String'] | null),cronTriggerSettings?: (Scalars['JSON'] | null),databaseEventTriggerSettings?: (Scalars['JSON'] | null),httpRouteTriggerSettings?: (Scalars['JSON'] | null),toolTriggerSettings?: (Scalars['JSON'] | null),workflowActionTriggerSettings?: (Scalars['JSON'] | null)}
export interface CreateCommandMenuItemInput {workflowVersionId?: (Scalars['UUID'] | null),frontComponentId?: (Scalars['UUID'] | null),engineComponentKey: EngineComponentKey,label: Scalars['String'],icon?: (Scalars['String'] | null),shortLabel?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isPinned?: (Scalars['Boolean'] | null),availabilityType?: (CommandMenuItemAvailabilityType | null),hotKeys?: (Scalars['String'][] | null),conditionalAvailabilityExpression?: (Scalars['String'] | null),availabilityObjectMetadataId?: (Scalars['UUID'] | null),payload?: (Scalars['JSON'] | null),pageLayoutId?: (Scalars['UUID'] | null)}
export interface UpdateCommandMenuItemInput {id: Scalars['UUID'],label?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),shortLabel?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isPinned?: (Scalars['Boolean'] | null),availabilityType?: (CommandMenuItemAvailabilityType | null),availabilityObjectMetadataId?: (Scalars['UUID'] | null),engineComponentKey?: (EngineComponentKey | null),hotKeys?: (Scalars['String'][] | null),pageLayoutId?: (Scalars['UUID'] | null)}
export interface CreateFrontComponentInput {id?: (Scalars['UUID'] | null),name: Scalars['String'],description?: (Scalars['String'] | null),sourceComponentPath: Scalars['String'],builtComponentPath: Scalars['String'],componentName: Scalars['String'],builtComponentChecksum: Scalars['String']}
export interface UpdateFrontComponentInput {
/** The id of the front component to update */
id: Scalars['UUID'],
/** The front component fields to update */
update: UpdateFrontComponentInputUpdates}
export interface UpdateFrontComponentInputUpdates {name?: (Scalars['String'] | null),description?: (Scalars['String'] | null)}
export interface UpdateAgentInput {id: Scalars['UUID'],name?: (Scalars['String'] | null),label?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),description?: (Scalars['String'] | null),prompt?: (Scalars['String'] | null),modelId?: (Scalars['String'] | null),roleId?: (Scalars['UUID'] | null),responseFormat?: (Scalars['JSON'] | null),modelConfiguration?: (Scalars['JSON'] | null),evaluationInputs?: (Scalars['String'][] | null)}
export interface CreateOneObjectInput {
/** The object to create */
@@ -6381,9 +6355,35 @@ export interface DeleteOneIndexInput {
/** The id of the custom index to delete. */
id: Scalars['UUID']}
export interface CreateAgentInput {name?: (Scalars['String'] | null),label: Scalars['String'],icon?: (Scalars['String'] | null),description?: (Scalars['String'] | null),prompt: Scalars['String'],modelId: Scalars['String'],roleId?: (Scalars['UUID'] | null),responseFormat?: (Scalars['JSON'] | null),modelConfiguration?: (Scalars['JSON'] | null),evaluationInputs?: (Scalars['String'][] | null)}
export interface CreateLogicFunctionFromSourceInput {id?: (Scalars['UUID'] | null),universalIdentifier?: (Scalars['UUID'] | null),name: Scalars['String'],description?: (Scalars['String'] | null),timeoutSeconds?: (Scalars['Float'] | null),source?: (Scalars['JSON'] | null),cronTriggerSettings?: (Scalars['JSON'] | null),databaseEventTriggerSettings?: (Scalars['JSON'] | null),httpRouteTriggerSettings?: (Scalars['JSON'] | null),toolTriggerSettings?: (Scalars['JSON'] | null),workflowActionTriggerSettings?: (Scalars['JSON'] | null)}
export interface UpdateAgentInput {id: Scalars['UUID'],name?: (Scalars['String'] | null),label?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),description?: (Scalars['String'] | null),prompt?: (Scalars['String'] | null),modelId?: (Scalars['String'] | null),roleId?: (Scalars['UUID'] | null),responseFormat?: (Scalars['JSON'] | null),modelConfiguration?: (Scalars['JSON'] | null),evaluationInputs?: (Scalars['String'][] | null)}
export interface ExecuteOneLogicFunctionInput {
/** Id of the logic function to execute */
id: Scalars['UUID'],
/** Payload in JSON format */
payload: Scalars['JSON']}
export interface UpdateLogicFunctionFromSourceInput {
/** Id of the logic function to update */
id: Scalars['UUID'],
/** The logic function updates */
update: UpdateLogicFunctionFromSourceInputUpdates}
export interface UpdateLogicFunctionFromSourceInputUpdates {name?: (Scalars['String'] | null),description?: (Scalars['String'] | null),timeoutSeconds?: (Scalars['Float'] | null),sourceHandlerCode?: (Scalars['String'] | null),handlerName?: (Scalars['String'] | null),sourceHandlerPath?: (Scalars['String'] | null),cronTriggerSettings?: (Scalars['JSON'] | null),databaseEventTriggerSettings?: (Scalars['JSON'] | null),httpRouteTriggerSettings?: (Scalars['JSON'] | null),toolTriggerSettings?: (Scalars['JSON'] | null),workflowActionTriggerSettings?: (Scalars['JSON'] | null)}
export interface CreateCommandMenuItemInput {workflowVersionId?: (Scalars['UUID'] | null),frontComponentId?: (Scalars['UUID'] | null),engineComponentKey: EngineComponentKey,label: Scalars['String'],icon?: (Scalars['String'] | null),shortLabel?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isPinned?: (Scalars['Boolean'] | null),availabilityType?: (CommandMenuItemAvailabilityType | null),hotKeys?: (Scalars['String'][] | null),conditionalAvailabilityExpression?: (Scalars['String'] | null),availabilityObjectMetadataId?: (Scalars['UUID'] | null),payload?: (Scalars['JSON'] | null),pageLayoutId?: (Scalars['UUID'] | null)}
export interface UpdateCommandMenuItemInput {id: Scalars['UUID'],label?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),shortLabel?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isPinned?: (Scalars['Boolean'] | null),availabilityType?: (CommandMenuItemAvailabilityType | null),availabilityObjectMetadataId?: (Scalars['UUID'] | null),engineComponentKey?: (EngineComponentKey | null),hotKeys?: (Scalars['String'][] | null),pageLayoutId?: (Scalars['UUID'] | null)}
export interface CreateFrontComponentInput {id?: (Scalars['UUID'] | null),name: Scalars['String'],description?: (Scalars['String'] | null),sourceComponentPath: Scalars['String'],builtComponentPath: Scalars['String'],componentName: Scalars['String'],builtComponentChecksum: Scalars['String']}
export interface UpdateFrontComponentInput {
/** The id of the front component to update */
id: Scalars['UUID'],
/** The front component fields to update */
update: UpdateFrontComponentInputUpdates}
export interface UpdateFrontComponentInputUpdates {name?: (Scalars['String'] | null),description?: (Scalars['String'] | null)}
export interface SendEmailViaDomainInput {emailingDomainId: Scalars['String'],to: Scalars['String'][],cc?: (Scalars['String'][] | null),bcc?: (Scalars['String'][] | null),subject: Scalars['String'],text: Scalars['String'],html?: (Scalars['String'] | null),from: Scalars['String'],replyTo?: (Scalars['String'][] | null)}
@@ -6186,57 +6186,15 @@ export default {
]
}
],
"findOneLogicFunction": [
41,
"findManyAgents": [
25
],
"findOneAgent": [
25,
{
"input": [
338,
"LogicFunctionIdInput!"
]
}
],
"findManyLogicFunctions": [
41
],
"getAvailablePackages": [
15,
{
"input": [
338,
"LogicFunctionIdInput!"
]
}
],
"getLogicFunctionSourceCode": [
1,
{
"input": [
338,
"LogicFunctionIdInput!"
]
}
],
"commandMenuItems": [
35
],
"commandMenuItem": [
35,
{
"id": [
3,
"UUID!"
]
}
],
"frontComponents": [
34
],
"frontComponent": [
34,
{
"id": [
3,
"UUID!"
"AgentIdInput!"
]
}
],
@@ -6287,15 +6245,57 @@ export default {
]
}
],
"findManyAgents": [
25
],
"findOneAgent": [
25,
"findOneLogicFunction": [
41,
{
"input": [
339,
"AgentIdInput!"
"LogicFunctionIdInput!"
]
}
],
"findManyLogicFunctions": [
41
],
"getAvailablePackages": [
15,
{
"input": [
339,
"LogicFunctionIdInput!"
]
}
],
"getLogicFunctionSourceCode": [
1,
{
"input": [
339,
"LogicFunctionIdInput!"
]
}
],
"commandMenuItems": [
35
],
"commandMenuItem": [
35,
{
"id": [
3,
"UUID!"
]
}
],
"frontComponents": [
34
],
"frontComponent": [
34,
{
"id": [
3,
"UUID!"
]
}
],
@@ -6747,17 +6747,17 @@ export default {
1
]
},
"LogicFunctionIdInput": {
"AgentIdInput": {
"id": [
218
3
],
"__typename": [
1
]
},
"AgentIdInput": {
"LogicFunctionIdInput": {
"id": [
3
218
],
"__typename": [
1
@@ -7604,11 +7604,83 @@ export default {
]
}
],
"createOneAgent": [
25,
{
"input": [
406,
"CreateAgentInput!"
]
}
],
"updateOneAgent": [
25,
{
"input": [
407,
"UpdateAgentInput!"
]
}
],
"deleteOneAgent": [
25,
{
"input": [
338,
"AgentIdInput!"
]
}
],
"createOneObject": [
56,
{
"input": [
408,
"CreateOneObjectInput!"
]
}
],
"deleteOneObject": [
56,
{
"input": [
410,
"DeleteOneObjectInput!"
]
}
],
"updateOneObject": [
56,
{
"input": [
411,
"UpdateOneObjectInput!"
]
}
],
"createOneIndex": [
47,
{
"input": [
413,
"CreateOneIndexInput!"
]
}
],
"deleteOneIndex": [
47,
{
"input": [
416,
"DeleteOneIndexInput!"
]
}
],
"deleteOneLogicFunction": [
41,
{
"input": [
338,
339,
"LogicFunctionIdInput!"
]
}
@@ -7617,7 +7689,7 @@ export default {
41,
{
"input": [
406,
417,
"CreateLogicFunctionFromSourceInput!"
]
}
@@ -7626,7 +7698,7 @@ export default {
168,
{
"input": [
407,
418,
"ExecuteOneLogicFunctionInput!"
]
}
@@ -7635,7 +7707,7 @@ export default {
6,
{
"input": [
408,
419,
"UpdateLogicFunctionFromSourceInput!"
]
}
@@ -7644,7 +7716,7 @@ export default {
35,
{
"input": [
410,
421,
"CreateCommandMenuItemInput!"
]
}
@@ -7653,7 +7725,7 @@ export default {
35,
{
"input": [
411,
422,
"UpdateCommandMenuItemInput!"
]
}
@@ -7680,7 +7752,7 @@ export default {
34,
{
"input": [
412,
423,
"CreateFrontComponentInput!"
]
}
@@ -7689,7 +7761,7 @@ export default {
34,
{
"input": [
413,
424,
"UpdateFrontComponentInput!"
]
}
@@ -7703,78 +7775,6 @@ export default {
]
}
],
"createOneObject": [
56,
{
"input": [
415,
"CreateOneObjectInput!"
]
}
],
"deleteOneObject": [
56,
{
"input": [
417,
"DeleteOneObjectInput!"
]
}
],
"updateOneObject": [
56,
{
"input": [
418,
"UpdateOneObjectInput!"
]
}
],
"createOneIndex": [
47,
{
"input": [
420,
"CreateOneIndexInput!"
]
}
],
"deleteOneIndex": [
47,
{
"input": [
423,
"DeleteOneIndexInput!"
]
}
],
"createOneAgent": [
25,
{
"input": [
424,
"CreateAgentInput!"
]
}
],
"updateOneAgent": [
25,
{
"input": [
425,
"UpdateAgentInput!"
]
}
],
"deleteOneAgent": [
25,
{
"input": [
339,
"AgentIdInput!"
]
}
],
"sendEmailViaEmailingDomain": [
283,
{
@@ -10021,6 +10021,235 @@ export default {
1
]
},
"CreateAgentInput": {
"name": [
1
],
"label": [
1
],
"icon": [
1
],
"description": [
1
],
"prompt": [
1
],
"modelId": [
1
],
"roleId": [
3
],
"responseFormat": [
15
],
"modelConfiguration": [
15
],
"evaluationInputs": [
1
],
"__typename": [
1
]
},
"UpdateAgentInput": {
"id": [
3
],
"name": [
1
],
"label": [
1
],
"icon": [
1
],
"description": [
1
],
"prompt": [
1
],
"modelId": [
1
],
"roleId": [
3
],
"responseFormat": [
15
],
"modelConfiguration": [
15
],
"evaluationInputs": [
1
],
"__typename": [
1
]
},
"CreateOneObjectInput": {
"object": [
409
],
"__typename": [
1
]
},
"CreateObjectInput": {
"nameSingular": [
1
],
"namePlural": [
1
],
"labelSingular": [
1
],
"labelPlural": [
1
],
"description": [
1
],
"icon": [
1
],
"shortcut": [
1
],
"color": [
1
],
"skipNameField": [
6
],
"isRemote": [
6
],
"primaryKeyColumnType": [
1
],
"primaryKeyFieldMetadataSettings": [
15
],
"isLabelSyncedWithName": [
6
],
"__typename": [
1
]
},
"DeleteOneObjectInput": {
"id": [
3
],
"__typename": [
1
]
},
"UpdateOneObjectInput": {
"update": [
412
],
"id": [
3
],
"__typename": [
1
]
},
"UpdateObjectPayload": {
"labelSingular": [
1
],
"labelPlural": [
1
],
"nameSingular": [
1
],
"namePlural": [
1
],
"description": [
1
],
"icon": [
1
],
"shortcut": [
1
],
"color": [
1
],
"isActive": [
6
],
"labelIdentifierFieldMetadataId": [
3
],
"imageIdentifierFieldMetadataId": [
3
],
"isLabelSyncedWithName": [
6
],
"isSearchable": [
6
],
"__typename": [
1
]
},
"CreateOneIndexInput": {
"index": [
414
],
"__typename": [
1
]
},
"CreateIndexInput": {
"objectMetadataId": [
3
],
"fields": [
415
],
"indexType": [
48
],
"__typename": [
1
]
},
"CreateIndexFieldInput": {
"fieldMetadataId": [
3
],
"subFieldName": [
1
],
"__typename": [
1
]
},
"DeleteOneIndexInput": {
"id": [
3
],
"__typename": [
1
]
},
"CreateLogicFunctionFromSourceInput": {
"id": [
3
@@ -10075,7 +10304,7 @@ export default {
3
],
"update": [
409
420
],
"__typename": [
1
@@ -10235,7 +10464,7 @@ export default {
3
],
"update": [
414
425
],
"__typename": [
1
@@ -10252,235 +10481,6 @@ export default {
1
]
},
"CreateOneObjectInput": {
"object": [
416
],
"__typename": [
1
]
},
"CreateObjectInput": {
"nameSingular": [
1
],
"namePlural": [
1
],
"labelSingular": [
1
],
"labelPlural": [
1
],
"description": [
1
],
"icon": [
1
],
"shortcut": [
1
],
"color": [
1
],
"skipNameField": [
6
],
"isRemote": [
6
],
"primaryKeyColumnType": [
1
],
"primaryKeyFieldMetadataSettings": [
15
],
"isLabelSyncedWithName": [
6
],
"__typename": [
1
]
},
"DeleteOneObjectInput": {
"id": [
3
],
"__typename": [
1
]
},
"UpdateOneObjectInput": {
"update": [
419
],
"id": [
3
],
"__typename": [
1
]
},
"UpdateObjectPayload": {
"labelSingular": [
1
],
"labelPlural": [
1
],
"nameSingular": [
1
],
"namePlural": [
1
],
"description": [
1
],
"icon": [
1
],
"shortcut": [
1
],
"color": [
1
],
"isActive": [
6
],
"labelIdentifierFieldMetadataId": [
3
],
"imageIdentifierFieldMetadataId": [
3
],
"isLabelSyncedWithName": [
6
],
"isSearchable": [
6
],
"__typename": [
1
]
},
"CreateOneIndexInput": {
"index": [
421
],
"__typename": [
1
]
},
"CreateIndexInput": {
"objectMetadataId": [
3
],
"fields": [
422
],
"indexType": [
48
],
"__typename": [
1
]
},
"CreateIndexFieldInput": {
"fieldMetadataId": [
3
],
"subFieldName": [
1
],
"__typename": [
1
]
},
"DeleteOneIndexInput": {
"id": [
3
],
"__typename": [
1
]
},
"CreateAgentInput": {
"name": [
1
],
"label": [
1
],
"icon": [
1
],
"description": [
1
],
"prompt": [
1
],
"modelId": [
1
],
"roleId": [
3
],
"responseFormat": [
15
],
"modelConfiguration": [
15
],
"evaluationInputs": [
1
],
"__typename": [
1
]
},
"UpdateAgentInput": {
"id": [
3
],
"name": [
1
],
"label": [
1
],
"icon": [
1
],
"description": [
1
],
"prompt": [
1
],
"modelId": [
1
],
"roleId": [
3
],
"responseFormat": [
15
],
"modelConfiguration": [
15
],
"evaluationInputs": [
1
],
"__typename": [
1
]
},
"SendEmailViaDomainInput": {
"emailingDomainId": [
1
@@ -30,6 +30,7 @@ import { generateFakeValue } from 'src/engine/utils/generate-fake-value';
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
import { DEFAULT_ITERATOR_CURRENT_ITEM } from 'src/modules/workflow/workflow-builder/workflow-schema/constants/default-iterator-current-item.const';
import {
type BaseOutputSchema,
Leaf,
Node,
type OutputSchema,
@@ -134,14 +135,10 @@ export class WorkflowSchemaWorkspaceService {
};
}
case WorkflowActionType.AI_AGENT: {
return {
response: {
label: 'Response',
isLeaf: true,
type: 'string',
value: 'Response of the agent',
},
};
return this.computeAiAgentActionOutputSchema({
agentId: step.settings.input.agentId,
workspaceId,
});
}
case WorkflowTriggerType.WEBHOOK:
case WorkflowActionType.CODE:
@@ -379,6 +376,63 @@ export class WorkflowSchemaWorkspaceService {
return { success: { isLeaf: true, type: 'boolean', value: true } };
}
private async computeAiAgentActionOutputSchema({
agentId,
workspaceId,
}: {
agentId?: string;
workspaceId: string;
}): Promise<OutputSchema> {
const textResponseOutputSchema: OutputSchema = {
response: {
label: 'Response',
isLeaf: true,
type: 'string',
value: 'Response of the agent',
},
};
if (!isDefined(agentId)) {
return textResponseOutputSchema;
}
const { flatAgentMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatAgentMaps'],
},
);
const flatAgent = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: agentId,
flatEntityMaps: flatAgentMaps,
});
const responseFormat = flatAgent?.responseFormat;
if (responseFormat?.type !== 'json') {
return textResponseOutputSchema;
}
return Object.entries(responseFormat.schema.properties || {}).reduce(
(outputSchema, [propertyName, property]) => {
outputSchema[propertyName] = {
isLeaf: true,
type: property.type,
label: propertyName,
...(isDefined(property.description)
? { description: property.description }
: {}),
value: generateFakeValue(property.type),
};
return outputSchema;
},
{} as BaseOutputSchema,
);
}
private async computeFormActionOutputSchema({
formFieldMetadataItems,
workspaceId,
@@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
import { type ToolSet } from 'ai';
import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
import { AgentService } from 'src/engine/metadata-modules/ai/ai-agent/agent.service';
import { LogicFunctionFromSourceService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
@@ -24,6 +25,7 @@ import { createDeleteWorkflowVersionEdgeTool } from 'src/modules/workflow/workfl
import { createDeleteWorkflowVersionStepTool } from 'src/modules/workflow/workflow-tools/tools/delete-workflow-version-step.tool';
import { createGetWorkflowCurrentVersionTool } from 'src/modules/workflow/workflow-tools/tools/get-workflow-current-version.tool';
import { createListLogicFunctionToolsTool } from 'src/modules/workflow/workflow-tools/tools/list-logic-function-tools.tool';
import { createUpdateAgentTool } from 'src/modules/workflow/workflow-tools/tools/update-agent.tool';
import { createUpdateLogicFunctionSourceTool } from 'src/modules/workflow/workflow-tools/tools/update-logic-function-source.tool';
import { createUpdateWorkflowVersionPositionsTool } from 'src/modules/workflow/workflow-tools/tools/update-workflow-version-positions.tool';
import { createUpdateWorkflowVersionStepTool } from 'src/modules/workflow/workflow-tools/tools/update-workflow-version-step.tool';
@@ -48,6 +50,7 @@ export class WorkflowToolWorkspaceService {
recordPositionService: RecordPositionService,
logicFunctionFromSourceService: LogicFunctionFromSourceService,
flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
agentService: AgentService,
) {
this.deps = {
workflowVersionStepService,
@@ -61,6 +64,7 @@ export class WorkflowToolWorkspaceService {
recordPositionService,
logicFunctionFromSourceService,
flatEntityMapsCacheService,
agentService,
};
}
@@ -128,6 +132,7 @@ export class WorkflowToolWorkspaceService {
this.deps,
context,
);
const updateAgent = createUpdateAgentTool(this.deps, context);
const validateWorkflow = createValidateWorkflowTool(this.deps, context);
return {
@@ -146,6 +151,7 @@ export class WorkflowToolWorkspaceService {
[getWorkflowCurrentVersion.name]: getWorkflowCurrentVersion,
[updateLogicFunctionSource.name]: updateLogicFunctionSource,
[listLogicFunctionTools.name]: listLogicFunctionTools,
[updateAgent.name]: updateAgent,
[validateWorkflow.name]: validateWorkflow,
};
}
@@ -1,5 +1,6 @@
import {
workflowActionSchema,
WorkflowActionType,
workflowTriggerSchema,
} from 'twenty-shared/workflow';
import { v4 as uuidv4 } from 'uuid';
@@ -99,6 +100,7 @@ Common mistakes to avoid:
- Missing the "objectRecord" field in CREATE_RECORD actions
- Using "fieldsToUpdate" instead of "objectRecord" in CREATE_RECORD actions
- Including CODE steps in this tool — this tool does NOT create the underlying logic function needed by CODE steps. Instead, create the workflow without CODE steps first, then add CODE steps individually using create_workflow_version_step (which properly creates the logic function), then call update_logic_function_source to define the code.
- Including AI_AGENT steps in this tool — this tool does NOT create the underlying agent needed by AI_AGENT steps. Instead, create the workflow without AI_AGENT steps first, then add AI_AGENT steps individually using create_workflow_version_step (which properly creates the agent), then call update_agent to configure the agent.
IMPORTANT: The tool schema provides comprehensive field descriptions, examples, and validation rules. Always refer to the schema for:
- Field requirements and data types
@@ -134,6 +136,17 @@ The response includes a compact validation summary. For the full validation repo
WorkflowVersionStepExceptionCode.INVALID_REQUEST,
);
}
const aiAgentSteps = parameters.steps.filter(
(step) => step.type === WorkflowActionType.AI_AGENT,
);
if (aiAgentSteps.length > 0) {
throw new WorkflowVersionStepException(
'AI_AGENT steps cannot be created via create_complete_workflow because it does not create the underlying agent. Use create_workflow_version_step instead, then call update_agent to configure the agent.',
WorkflowVersionStepExceptionCode.INVALID_REQUEST,
);
}
const workflowId = await createWorkflow({
deps,
context,
@@ -86,6 +86,12 @@ const enrichResultWithNextStep = ({
nextStep:
'This CODE step was created with a default placeholder function. You MUST now call update_logic_function_source with the logicFunctionId from this step to define the actual code. IMPORTANT: Also provide outputSchema (an example return value, e.g. { datePlus7: "2026-06-16" }) so downstream steps can reference this step\'s output variables via {{stepId.fieldName}}.',
};
case WorkflowActionType.AI_AGENT:
return {
...result,
nextStep:
'This AI_AGENT step was created with a default placeholder agent. You MUST now call update_agent with the agentId from this step\'s settings.input.agentId to set the agent\'s system prompt (and optionally its model and responseFormat). Use responseFormat { type: "json", schema: { ... } } when downstream steps need to reference structured fields via {{stepId.fieldName}}, otherwise the output is referenced as {{stepId.response}}. If the step needs a task-specific prompt, also set it via update_workflow_version_step on settings.input.prompt.',
};
default:
return result;
}
@@ -0,0 +1,104 @@
import { isDefined } from 'twenty-shared/utils';
import { z } from 'zod';
import { type AgentResponseFormat } from 'src/engine/metadata-modules/ai/ai-agent/types/agent-response-format.type';
import { type ModelId } from 'src/engine/metadata-modules/ai/ai-models/types/model-id.type';
import {
type WorkflowToolContext,
type WorkflowToolDependencies,
} from 'src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type';
const agentResponseFormatSchema = z.union([
z.object({ type: z.literal('text') }),
z.object({
type: z.literal('json'),
schema: z.object({
type: z.literal('object'),
properties: z.record(
z.string(),
z.object({
type: z.enum(['string', 'number', 'boolean']),
description: z.string().optional(),
}),
),
required: z.array(z.string()).optional(),
additionalProperties: z.literal(false).optional(),
}),
}),
]);
const updateAgentSchema = z.object({
agentId: z
.string()
.uuid()
.describe(
"The ID of the agent to update (from the AI_AGENT step's settings.input.agentId)",
),
prompt: z
.string()
.optional()
.describe(
"The agent's system prompt describing its role, behavior and the task it must accomplish.",
),
modelId: z
.string()
.optional()
.describe(
'Optional model id to use for the agent. Leave empty to keep the auto-selected model.',
),
responseFormat: agentResponseFormatSchema
.optional()
.describe(
'Optional response format. Use { type: "text" } for free-form text output, or { type: "json", schema: { type: "object", properties: { fieldName: { type: "string" } } } } for structured output. Downstream steps can reference structured fields via {{stepId.fieldName}} (or {{stepId.response}} for text format).',
),
});
export const createUpdateAgentTool = (
deps: Pick<WorkflowToolDependencies, 'agentService'>,
context: WorkflowToolContext,
) => ({
name: 'update_agent' as const,
description: `Update the AI agent used by a workflow AI_AGENT step.
Use this tool to configure the agent created when an AI_AGENT step is added: set its system prompt, the model it should use, and the format of its output.
- prompt: the agent's system prompt (its role, behavior and task).
- modelId: optional model id; omit to keep the auto-selected model.
- responseFormat: { type: "text" } for free-form text (referenced as {{stepId.response}}), or { type: "json", schema: { ... } } for structured output whose fields can be referenced as {{stepId.fieldName}}.
To find the agentId, look at the AI_AGENT step's settings.input.agentId field.`,
inputSchema: updateAgentSchema,
execute: async (parameters: {
agentId: string;
prompt?: string;
modelId?: string;
responseFormat?: AgentResponseFormat;
}) => {
try {
const { agentId, prompt, modelId, responseFormat } = parameters;
const { workspaceId } = context;
const updatedAgent = await deps.agentService.updateOneAgent({
input: {
id: agentId,
...(isDefined(prompt) ? { prompt } : {}),
...(isDefined(modelId) ? { modelId: modelId as ModelId } : {}),
...(isDefined(responseFormat) ? { responseFormat } : {}),
},
workspaceId,
});
return {
success: true,
message: `Successfully updated agent ${agentId}`,
agentId: updatedAgent.id,
};
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to update agent: ${error.message}`,
};
}
},
});
@@ -1,4 +1,5 @@
import type { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
import type { AgentService } from 'src/engine/metadata-modules/ai/ai-agent/agent.service';
import type { LogicFunctionFromSourceService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source.service';
import type { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import type { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
@@ -22,6 +23,7 @@ export type WorkflowToolDependencies = {
recordPositionService: RecordPositionService;
logicFunctionFromSourceService: LogicFunctionFromSourceService;
flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService;
agentService: AgentService;
};
export type WorkflowToolContext = {
@@ -2,6 +2,7 @@ import { Global, Module } from '@nestjs/common';
import { RecordPositionModule } from 'src/engine/core-modules/record-position/record-position.module';
import { WORKFLOW_TOOL_SERVICE_TOKEN } from 'src/engine/core-modules/tool-provider/constants/workflow-tool-service.token';
import { AiAgentModule } from 'src/engine/metadata-modules/ai/ai-agent/ai-agent.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
import { WorkflowSchemaModule } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.module';
@@ -27,6 +28,7 @@ import { WorkflowToolWorkspaceService } from './services/workflow-tool.workspace
RecordPositionModule,
LogicFunctionModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
AiAgentModule,
],
providers: [
WorkflowToolWorkspaceService,