## Context
Deprecating the old objectMetadataMap type in favour of split flat
entities to match with our new caching.
In the long run, trying to achieve:
- Better performance through caching
- Consistent data access patterns across the codebase
- Reduced database queries
Now that everything is based on flat entities, which are cached, we can
finish the refactoring of workspace context cache which should already
improve performances.
Then the last step will be to consume that new cache in the new global
datasource to get rid of the many workspace datasources stored in the
server
## Summary
This PR introduces a comprehensive agent evaluation system and refactors
the AI module structure for better organization.
## Key Changes
### 🎯 Agent Evaluation System
- Added **Agent Turn Evaluation** entities, DTOs, and database schema
- New GraphQL mutations: `evaluateAgentTurn` and `runEvaluationInput`
- Added `evaluationInputs` field to Agent entity for storing test inputs
- New `AgentTurnGraderService` for automatic turn evaluation
- Added evaluation UI with new **Evals** and **Logs** tabs in agent
detail pages
### 🏗️ Entity & Module Refactoring
- Renamed `AgentChatMessage` → `AgentMessage` for clarity
- Consolidated chat entities: `AgentMessage`, `AgentTurn`, and
`AgentChatThread`
- Reorganized AI modules under `ai/` subdirectory structure
- Updated imports across codebase to reflect new module paths
### 🤖 New Agents & Roles
- Added **Dashboard Builder Agent** for dashboard creation and
management
- Added **Dashboard Manager Role** with appropriate permissions
- Updated role permissions to be more granular (users vs agents vs API
keys)
### 🔐 Permission System Updates
- Added `HTTP_REQUEST_TOOL` permission flag
- Updated Workflow Manager role permissions (restricted tool access)
- Enhanced permission flag types to differentiate between user/agent/API
key contexts
- Added `isRelevantForAgents`, `isRelevantForApiKeys`,
`isRelevantForUsers` to permission flags
### 📨 Message Role Enhancement
- Added `system` role to `AgentMessageRole` enum (alongside
user/assistant)
- Updated message handling to support system prompts
### 🎨 UI/UX Improvements
- New tabs in agent detail: **Evals** and **Logs**
- Added turn detail page: `/ai/agents/:agentId/turns/:turnId`
- Fixed text overflow in `SettingsListItemCardContent`
- Updated role applicability labels ("Assignable to Workspace Members")
### 🛠️ Technical Improvements
- Fixed Zod schema validation for UUID and Date fields (use string
validators)
- Updated `ToolRegistryService` to properly register HTTP tool with
permission flag
- Enhanced error handling in agent execution services
- Updated database migrations for new entity schema
## Database Migrations
- `1764210000000-add-system-role-to-agent-message.ts`
- `1764220000000-add-evaluation-inputs-to-agent.ts`
- `1764200000000-add-agent-turn-evaluation.ts`
- `1764100000000-refactor-agent-chat-entities.ts`
## Testing
- [ ] Agent evaluation flow tested
- [ ] Dashboard Builder agent tested
- [ ] Permission system validated
- [ ] UI tabs and navigation tested
- [ ] Database migrations run successfully
## Breaking Changes
⚠️ **Entity Rename**: `AgentChatMessage` renamed to `AgentMessage` -
GraphQL queries need updating
## Related Issues
<!-- Link any related issues here -->
## Screenshots
<!-- Add screenshots if applicable -->
# Introduction
We need to be able to create custom workspace application on all
workspaces, even pending and ongoing etc
Right now the upgrade devx only allows and expect active or suspended
workspace to be passed to runOnWorkspace.
## WorkspacesMigrationRunner
Created an intermediate class `WorkspacesMigrationRunner` that expect an
array `WorkspaceStatus` to be fetched for the current command to be run
on
The `ActiveOrSuspendedCommandRunner` statically passes both `SUSPENDED`
and `ACTIVE`, whereas the create workspace custom application passed all
the enum values
## DataSource
Workspace that are not fully init don't have a `workspace_schema` so
they don't have `dataSource`
Made a not very elegant check to see if current workspace we're about to
create dataSource on has one historically
Which means that dataSource is now optional, it had only one impact on
an existing command and the desired devx will become consuming existing
services that do not expect dataSource ( or at least yet )
Closes https://github.com/twentyhq/twenty/issues/15692, fixes
https://github.com/twentyhq/twenty/issues/14678
The issue was:
- When a user is signing up, they are asked for their first name and
last name with which we fulfill their workspaceMember entity for the
workspace they are signing up to (which is the one they are creating if
they are not joining an existing workspace). user.firstName and
user.lastName remain empty
- When we create a record, we are using user.firstName and user.lastName
to fulfill the text field "createdByName", which intends to save the
name of the creator at the moment of the creation. This field is then
use 1) when filtering on createdBy (as a trick to be able to filter by
this, since we don't have filters on nested fields, ie we dont have
filters on person.createdByWorkspaceMember) 2) to display the user
creator when a user has left a workspace - otherwise we rely on the
dynamic createdByWorkspaceMember.firstName and lastName
- So filtering on createdBy was not working as createdByName is empty.
We did not see that in dev mode because we have seeded users with names
The fix
- When we create a record, we use workspaceMember.firstName and
workspaceMember.lastName as we should.
- When an existing user joins a workspace, they are asked to give their
name again so that we set their workspaceMember name
- This will not fix the history of the records (they will still have
createdByName empty so the filter will not work properly), but a command
to fix it would be very long as it would iterate over all the records of
each active workspace
The long-term view is to get rid of user and to store the name in
userWorkspace
We have introduced to new syncStage statuses:
`messageChannel.MESSAGES_IMPORT_SCHEDULED` and
`calendarChannel.CALENDAR_EVENTS_IMPORT_SCHEDULED`
We need to make sure all existing workspaces have it
@charlesBochet
In workflow codebase, NULL (instead of empty object) is expected on
workflow version object step field, when a new workflow is created for
example.
(packages/twenty-front/src/modules/workflow/workflow-diagram/utils/generateWorkflowDiagram.ts
- 42)
This case is not isolated and it creates many issues.
We decided to format NULL value to equivalent (empty string for text
field, empty object for raw_json) but it seems it complicates the dev x.
To unlock @Devessier I prefer revert the logic, the time we discuss how
to solve this cases.
## Overview
This PR replaces the dynamic agent handoff system with a more
predictable planning-based router that decides upfront how to handle
multi-agent coordination.
## Major Changes
### 🔄 Architecture Shift: Handoffs → Planning
**Removed:**
- `AgentHandoffEntity` and handoff tracking system
- `AgentHandoffService` and `AgentHandoffExecutorService`
- Dynamic agent-to-agent transfers during execution
- Handoff tool generation and description templates
**Added:**
- `AiRouterService` with two strategies: `simple` (single agent) and
`planned` (multi-agent)
- `AgentPlanExecutorService` for executing multi-step plans
- Plan validation (cycle detection, dependency resolution)
- `UnifiedRouterResult` type with discriminated union
### 🤖 New Standard Agents
Added two new specialized agents:
- **Researcher Agent**: Web search, fact-finding, competitive
intelligence
- **Code Agent**: TypeScript function generation for serverless
workflows
### 🏗️ Router Refactoring (Latest)
Split router responsibilities into focused services:
- `AiRouterStrategyDeciderService`: Decides simple vs planned strategy
- `AiRouterPlanGeneratorService`: Generates and validates execution
plans
- `AiRouterService`: Coordinates between services (reduced from 426→275
lines)
### ⚙️ Configuration Improvements
- Added `outputStrategy` to agent definitions (`direct` vs `synthesize`)
- Removed hardcoded special cases for workflow-builder
- Added `plannerModel` field to workspace entity
- Increased `MAX_STEPS` from 10 to 25 for complex workflows
### 📝 Agent Prompt Refinements
Significantly simplified prompts for better clarity:
- Workflow Builder: 51→36 lines
- Helper: 49→28 lines
- Data Manipulator: Enhanced with sorting guidance
### 🔍 Enhanced Debugging
- Plan reasoning and step count in data message parts
- Router debug info with token usage tracking
- Better logging throughout execution pipeline
## Benefits
1. **Simpler Mental Model**: Router decides upfront vs dynamic transfers
2. **Better Predictability**: Users see the plan before execution
3. **Cleaner Architecture**: SRP with focused services
4. **Configuration Over Code**: Agent behavior via config, not hardcoded
logic
5. **Plan Validation**: Catches invalid dependencies and cycles
## Migration Notes
- Database migration removes `agentHandoff` table
- Adds `plannerModel` column to workspace table
- No API breaking changes (agent endpoints unchanged)
## Testing
- Integration tests updated to remove handoff dependencies
- Agent tool test utilities simplified
- Plan validation covered by new logic
## Next Steps (Future PRs)
- Parallel execution of independent plan steps
- Dynamic re-planning based on results
- Plan caching for common routing patterns
- Error recovery strategies in plan executor
Block users from creating NUMERIC, POSITION, and TS_VECTOR fields via
the API as these are system-only types. Users should use NUMBER instead
of NUMERIC.
/closes #16023
## Context
Deprecating legacy ObjectMetadata from cache in favor of flat entities.
Introducing utils to build byName/byNameSingular/byNamePlural in
isolated cases
## Next
- I had to introduce a util to build from flat to legacy
objectMetadataMaps, we should instead use flat maps directly when needed
(datasource, schema generation, etc)
- Deprecate metadata version in the cache
- Use the new cache strategy for flat entities with permissions and
feature flags and inject in the global datasource context
closes https://github.com/twentyhq/core-team-issues/issues/1629
To do before requesting review :
- filter update
Migration to come in an other PR
Strat :
1/ Null transformation
- [x] Transform NULL equivalent value to NULL in field validation in
common api - pre-query - with feature flag
- [ ] Same logic in ORM (Not done, complex to handle feature flag here)
- [x] Transform NULL value to equivalent in data formatting in ORM -
post-query
2/ Migration (in other PR) for fieldMetadata not nullable with default
defaultValue (empty string, ...)
- [ ] Remove NOT NULL db constraint
- [ ] Update record value to NULL
- [ ] Update field metadata : isNullable:true
- [ ] Update uniqueIndex whereClause (also for standard uniqueIndex)
- [ ] Activate feature flag
3/ Update metadata creation
- [x] No more default default value
- [x] Update standard field nullability
- [x] Remove index default whereClause for standard field
4/ Update filter
- [x] When filtering on NULL or empty string, be sure all records are
returned (the one with NULL + the one with "")
5/ Test
- [ ] Strat. to do
# Introduction
Two things:
- Enforcing non nullable workspace custom application Id for any
workspace
- Fixing front non editable data models following
https://github.com/twentyhq/twenty/pull/15911 that associate any custom
entities to an applicationId. The front was putting everything as
readonly when under an app ( we will have to handle the twenty standard
application in the future too )
## Fallback
### Migration
The non nullable migration will fail when released, that's why it's
being swallowed and re-run in an upgrade command post workspace custom
application creation for those that miss one. Allowing the migration to
pass in the end
The typeorm migration still need to exists for any new workspaces
### GetCurrentUser
In order to dynamically display isReadOnly in data model settings we're
fetching the workspaceCustomApplicationId through the `getCurrentUser`
If not fallback this endpoint would throw until we're handling existing
workspaces that do not have a custom workspace application
The fallback should be removed post release
# Introduction
Related https://github.com/twentyhq/twenty/issues/15846
The root cause is that universalIdentifier is still optional in database
and fallbacked when extracted out of database to standardId. But all
`BaseWorkspaceEntity` and `CustomWorkspaceEntity` share the same
standardId for their default standard fields `createdAt` `deletedAt`
resulting in such compare result in dispatcher
```ts
{
"initialDispatcher": {
"createdFlatEntityMaps": {
"byId": {},
"idByUniversalIdentifier": {},
"universalIdentifiersByApplicationId": {}
},
"deletedFlatEntityMaps": {
"byId": {},
"idByUniversalIdentifier": {},
"universalIdentifiersByApplicationId": {}
},
"updatedFlatEntityMaps": {
"byId": {
"55e1568c-eb87-4b8a-9f1b-19bbf6042f3e": {
"updates": [
{
"from": "Deletion date",
"to": "Date when the record was deleted",
"property": "description"
},
{
"from": "IconCalendarClock",
"to": "IconCalendarMinus",
"property": "icon"
},
{
"from": false,
"to": true,
"property": "isLabelSyncedWithName"
},
{
"from": null,
"to": {
"displayFormat": "RELATIVE"
},
"property": "settings"
}
]
}
}
}
},
"fromFlatEntity": {
"universalIdentifier": "20202020-b9a7-48d8-8387-b9a3090a50ec",
"applicationId": null,
"id": "9c97c8bf-1f64-463c-915c-f68f41d3cd60",
"standardId": "20202020-b9a7-48d8-8387-b9a3090a50ec",
"objectMetadataId": "e9565126-8351-457b-b003-3ea4c6d253bc",
"type": "DATE_TIME",
"name": "deletedAt",
"label": "Deleted at",
"defaultValue": null,
"description": "Deletion date",
"icon": "IconCalendarClock",
"standardOverrides": null,
"options": null,
"settings": null,
"isCustom": false,
"isActive": true,
"isSystem": false,
"isUIReadOnly": true,
"isNullable": true,
"isUnique": false,
"workspaceId": "20202020-1c25-4d02-bf25-6aeccf7ea419",
"isLabelSyncedWithName": false,
"relationTargetFieldMetadataId": null,
"relationTargetObjectMetadataId": null,
"morphId": null,
"createdAt": "2025-11-20T17:28:45.474Z",
"updatedAt": "2025-11-20T17:28:45.474Z",
"kanbanAggregateOperationViewIds": [],
"calendarViewIds": [],
"viewGroupIds": [],
"viewFieldIds": [],
"viewFilterIds": []
},
"toFlatEntity": {
"universalIdentifier": "20202020-b9a7-48d8-8387-b9a3090a50ec",
"applicationId": null,
"id": "55e1568c-eb87-4b8a-9f1b-19bbf6042f3e",
"standardId": "20202020-b9a7-48d8-8387-b9a3090a50ec",
"objectMetadataId": "37263f48-6858-4d28-a6e1-5f7321e49c24",
"type": "DATE_TIME",
"name": "deletedAt",
"label": "Deleted at",
"defaultValue": null,
"description": "Date when the record was deleted",
"icon": "IconCalendarMinus",
"standardOverrides": null,
"options": null,
"settings": {
"displayFormat": "RELATIVE"
},
"isCustom": false,
"isActive": true,
"isSystem": false,
"isUIReadOnly": true,
"isNullable": true,
"isUnique": false,
"workspaceId": "20202020-1c25-4d02-bf25-6aeccf7ea419",
"isLabelSyncedWithName": true,
"relationTargetFieldMetadataId": null,
"relationTargetObjectMetadataId": null,
"morphId": null,
"createdAt": "2025-11-20T17:28:44.267Z",
"updatedAt": "2025-11-21T17:17:55.057Z",
"kanbanAggregateOperationViewIds": [],
"calendarViewIds": [],
"viewGroupIds": [],
"viewFieldIds": [],
"viewFilterIds": []
}
}
```
## Impact
- This might be corrupting label and description of an other standard
field of an other object
- Race condition on latest universalIdentifier assigned in cache making
the update sometime accurate sometimes not
## Fix
Will be fixed by the in coming work on applicationId and
universalIdentifier as required in database + upgrade command that will
handle retro-comp. ( won't handle description corruption though, should
be anecdotical )
https://github.com/twentyhq/twenty/pull/15911 ( handling this only for
new workspace, retro comp upgrade command will be coming just after )
## PR scope
- Introduce TDD integration tests as failing
- Added unit test to critical methods that might have been involved in
the root cause ( still worth it to keep )
---------
Co-authored-by: guillim <guigloo@msn.com>
# Introduction
Cleaner and fewer scope version of
https://github.com/twentyhq/twenty/pull/15745 ( removed sync-metadata
hack through, too ambitious migration and upgrade )
Please note that this PR won't have any interaction with the existing
sync-metadata
Which mean that the sync metadata does not update the standard entities
applicationId and universalIdentifier, and it won't we will deprecate it
on favor of a workspace migration aka twenty-standard app installation
## API Metadata
Any operation going through the api metadata nows automatically scope
the related entity to the workspace custom application instance. (
optionally passing an applicationId to allow current hacky implem of app
sync service )
We need to either ignore the tests or remove the cli status check from
the blocking status badges for a PR to be merged
## New workspace
Already handled in previous
https://github.com/twentyhq/twenty/pull/15625, when a workspace is
created it gets created a twenty standard and custom workspace instance
All his views and permissions will be prefilled to the its twenty
standard app instance with a specific universalIdentifier
## New universalIdentifier
At the contrary as before with standardIds, universalIdentifier are
unique for a given workspace
This means that createdAt field of both object company and opportunity
will have a unique universalIdentifier whereas they share the same
standardId
## FlatApplication
Introduced the flatApplication and cache. Will migrate existing
`MetadataName` to be `SyncableMetadataName` in a following PR
## What's next
Next we will describe a twenty standard app configuration as json that
will be used to generate a workspace migration that will be run instead
of the sync metadata, in a nutshell we aim to deprecated the sync
metadata
So we can standardize any entity to have a non nullable applicationId
and universalIdentifier
## Upgrade command
Introduced an upgrade command that will create a custom workspace
instance for any workspace that do not have one in order to align with
the new behavior when creating a new workspace
UpdateOne of a Relation that involves a CustomObject, because the
nameSingular needs to be updated in the fieldMetadata
- nameSingular and namePlural must be provided since they are necessary
for morph name computation
- label sync should be false
Interesting files to look at:
-
packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-relation-flat-field-metadatas-for-custom-object.util.ts
- UPDATE =>
packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/rename-related-morph-field-on-object-names-update.util.ts
( also update relation indexes ) needs v2 refactor to handle field
relation name update
- CREATE =>
packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-relation-flat-field-metadatas-for-custom-object.util.ts
( handle morph instead of previous classic relation )
- DELETE => DONE
Edit:
closes https://github.com/twentyhq/core-team-issues/issues/1897
---------
Co-authored-by: prastoin <paul@twenty.com>