## Context
- All flatEntity should extend SyncableEntity
- SyncableEntity should now have applicationId and application relation
- Fix syncApp deletion, should now properly use migration v2 to delete
syncable entities
# Introduction
### Summary
Implements side effect handling for `ViewGroup` and `ViewFilters` when
field metadata is updated in the v2 architecture. This ensures that
view-related records are properly maintained when enum field options are
modified, deleted, or created.
### Side effects
- **Side Effect System**: Added side effect handling for field metadata
updates that manages related view groups and view filters
- **Enum Field Updates**: When enum field options are modified, the
system now:
- **View Groups**: Creates new groups for added options, updates
existing groups for modified options, and deletes groups for removed
options
- **View Filters**: Updates filter values to reflect option changes and
removes filters that reference deleted options
### Enum runner fix
Update now works for both atomic enum and array enum ( multi select for
instance )
### Compute flat entity maps from to
Standardized this method usage across v2 services
Next step is to require dependencies dynamically
## Conclusion
closes https://github.com/twentyhq/core-team-issues/issues/1649
Improvements to database seeding performance and developer experience.
**Changes:**
1. **Attachment seeding**: Add sample files (PDF, XLSX, PPTX, PNG, ZIP)
to dev seeds with proper file storage
2. **Seeding parallelization**: Process entities within batches in
parallel while respecting dependencies
3. **ORM query logging**: Replace manual logger toggling with
`ORM_QUERY_LOGGING` env var
- Values: `disabled` (default), `server-only` (for local dev), `always`
- Configured once in `core.datasource.ts`, removed from all seeder
services
**For .env:**
```bash
ORM_QUERY_LOGGING=server-only
```
Net result: Faster seeding, cleaner code (-68 lines), better local dev
experience.
I had an issue with invalid UUIDs, and I think it would be easier to
find the offending one if the UUID were included in the error message.
This way a database dump can be easily searched.
---------
Co-authored-by: prastoin <paul@twenty.com>
This commit fixes a PostgresException error that occurred when
processing timeline activities with empty workspaceMemberId values.
workspaceMemberId appears to be optional but we set it to an empty
string when it is not present. This subsequently causes issues in the
postgres query.
This change fixes the root cause. Empty string handling is added to the
postgres query also for situations where bad data has already entered
the db
Example error
```
[Nest] 34 - 10/13/2025, 9:09:55 PM LOG [BullMQDriver] Job 2274 with name MessageParticipantMatchParticipantJob processed on queue messaging-queue
query failed: SELECT "timelineActivity"."happensAt" AS "timelineActivity_happensAt", "timelineActivity"."name" AS "timelineActivity_name", "timelineActivity"."properties" AS "timelineActivity_properties", "timelineActivity"."linkedRecordCachedName" AS "timelineActivity_linkedRecordCachedName", "timelineActivity"."linkedRecordId" AS "timelineActivity_linkedRecordId", "timelineActivity"."linkedObjectMetadataId" AS "timelineActivity_linkedObjectMetadataId", "timelineActivity"."id" AS "timelineActivity_id", "timelineActivity"."createdAt" AS "timelineActivity_createdAt", "timelineActivity"."updatedAt" AS "timelineActivity_updatedAt", "timelineActivity"."deletedAt" AS "timelineActivity_deletedAt", "timelineActivity"."workspaceMemberId" AS "timelineActivity_workspaceMemberId", "timelineActivity"."personId" AS "timelineActivity_personId", "timelineActivity"."companyId" AS "timelineActivity_companyId", "timelineActivity"."opportunityId" AS "timelineActivity_opportunityId", "timelineActivity"."noteId" AS "timelineActivity_noteId", "timelineActivity"."taskId" AS "timelineActivity_taskId", "timelineActivity"."workflowId" AS "timelineActivity_workflowId", "timelineActivity"."workflowVersionId" AS "timelineActivity_workflowVersionId", "timelineActivity"."workflowRunId" AS "timelineActivity_workflowRunId" FROM "workspace_8h07bh3zq5pjg65lx9qbxbgg0"."timelineActivity" "timelineActivity" WHERE ( (("timelineActivity"."noteId" IN ($1, $2)) AND ("timelineActivity"."name" IN ($3, $4)) AND ("timelineActivity"."workspaceMemberId" IN ($5, $6)) AND ("timelineActivity"."createdAt" > $7)) ) AND ( "timelineActivity"."deletedAt" IS NULL ) ORDER BY "timelineActivity"."createdAt" DESC LIMIT 1 -- PARAMETERS: [null,"aa6f2e7f-16b1-447f-9988-0ba359358609","linked-note.created","note.created","",null,"2025-10-13T20:59:55.267Z"]
error: error: invalid input syntax for type uuid: ""
/app/packages/twenty-server/dist/src/engine/twenty-orm/error-handling/compute-twenty-orm-exception.js:36
throw new _postgresexception.PostgresException(error.message, errorCode);
^
PostgresException [Error]: invalid input syntax for type uuid: ""
at computeTwentyORMException (/app/packages/twenty-server/dist/src/engine/twenty-orm/error-handling/compute-twenty-orm-exception.js:36:19)
at WorkspaceSelectQueryBuilder.getMany (/app/packages/twenty-server/dist/src/engine/twenty-orm/repository/workspace-select-query-builder.js:56:76)
at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
at async WorkspaceRepository.find (/app/packages/twenty-server/dist/src/engine/twenty-orm/repository/workspace.repository.js:33:24)
at async TimelineActivityRepository.findRecentTimelineActivities (/app/packages/twenty-server/dist/src/modules/timeline/repositories/timeline-activity.repository.js:68:16)
at async TimelineActivityRepository.upsertTimelineActivities (/app/packages/twenty-server/dist/src/modules/timeline/repositories/timeline-activity.repository.js:27:42) {
code: '22P02'
}
```
This migration updates the filter operand values of the workflowVersion
and workflowRuns in order to capitalize them as they should be (the
product works with both deprecated camel case and capitalized). But that
may involve thousands of workflowRuns!
Let's update the command to only update workflowVersion, and update the
cleanWorkflowRuns job to remove workflow runs that are more than 14 days
old.
This way after the command is run, all new workflow runs will have the
new value for the filter operand, and after fourteen days there will be
no trace of the workflow runs with the deprecated filter operand.
## Goal
- inferDeletionFromMissingEntities is now a map instead of a single
bool, allowing us to parameterise it based on the entity we want to
compare
- Adding index creation/update when field isUnique is set to true and
index deletion when isUnique is false (for now)
close https://github.com/twentyhq/core-team-issues/issues/1346
### Summary
Split BAR into VERTICAL_BAR and HORIZONTAL_BAR as separate chart types.
### The Problem
Initially wanted a simple vertical/horizontal toggle for bar charts, but
ran into a GraphQL union type
constraint: union types can't have the same field name with different
nullability.
- Vertical bars need groupByFieldMetadataIdX as required (categories on
X)
- Horizontal bars need groupByFieldMetadataIdY as required (categories
on Y)
- GraphQL schema generation fails with this setup
### The Solution
Use semantic primaryAxis and secondaryAxis naming that's
orientation-agnostic:
- primaryAxisGroupByFieldMetadataId = main grouping field (e.g.,
"Company Name")
- secondaryAxisGroupByFieldMetadataId = optional secondary grouping
(e.g., "Stage")
These fields have consistent meaning regardless of orientation. The
visual mapping happens at the UI layer:
- Vertical bars: primary data renders on X-axis, secondary on Y-axis
- Horizontal bars: primary data renders on Y-axis, secondary on X-axis
Both chart types share the same DTO structure with consistent
nullability.
### What Changed
- Split GraphType.BAR → VERTICAL_BAR | HORIZONTAL_BAR
- Renamed fields: primaryAxisGroupByFieldMetadataId,
secondaryAxisGroupByFieldMetadataId (+ subfield
variants)
- useChartSettingsValues(): Maps semantic fields to setting values (no
swapping)
- getBarChartSettings(): Dynamically arranges settings panel based on
orientation
- transformGroupByDataToBarChartData(): Maps semantic fields to Nivo's
layout prop
video QA
https://github.com/user-attachments/assets/479061b5-712e-4ca6-9858-95273d1f16c1
Closes#14726
### Added
- `trashRetentionDays` field to workspace entity (default: 14 days)
- Automated trash cleanup using BullMQ jobs
- Daily cron (00:10 UTC) that enqueues cleanup jobs for all active
workspaces
- Per-workspace limit: 100k records deleted per day
- Calendar-based retention: records deleted on day X are cleaned up X+14
days later (at midnight UTC boundaries)
### Architecture
- **Cron (WorkspaceTrashCleanupCronJob):** Runs daily, enqueues jobs in
parallel for all workspaces
- **Job (WorkspaceTrashCleanupJob):** Processes individual workspace
cleanup
- **Service (WorkspaceTrashCleanupService):** Discovers tables with
`deletedAt`, deletes old records with quota enforcement
- **Command:** `npx nx run twenty-server:command
cron:workspace:cleanup-trash` to register the cron
### Testing
- Unit tests for service with 100% coverage of public API
- Tested quota enforcement, error handling, and edge cases
---------
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
## Problem
When trying to signup on localhost:3001 with a new random email, users
were getting an error message saying 'User already exists', even though
they were new users.
## Root Cause
The `signUpWithoutWorkspace` method in `sign-in-up.service.ts` had
inverted logic when checking if a user exists. It was using
`findUserByEmailOrThrow` which:
- **Returns the user** if found
- **Throws the provided error** if NOT found
This caused the opposite behavior:
- ❌ **New user (doesn't exist)**: Threw 'User already exists' error
- ❌ **Existing user**: Continued to create duplicate user
## Solution
Changed to use `findUserByEmail` and explicitly check if the user exists
before throwing the appropriate error:
```typescript
const existingUser = await this.userService.findUserByEmail(newUserParams.email);
if (existingUser) {
throw new AuthException(
'User already exist',
AuthExceptionCode.USER_ALREADY_EXIST,
{ userFriendlyMessage: msg`User already exists` },
);
}
```
This matches the correct pattern already used in `signUpInWorkspace`
(line 431-441 in auth.resolver.ts).
## Changes
- Fixed inverted logic in `signUpWithoutWorkspace` method
- Now correctly validates that user does NOT exist before creating new
user
- Matches the pattern used in `signUpInWorkspace`
## Testing
The fix corrects the logic so that:
- ✅ New users can sign up successfully
- ✅ Existing users get the correct 'User already exists' error
I did an acceptable design for empty node and iterators for release:
- use array field for iterator node. Added an util to stringify arrays
for backward compatibily
- remove icon for empty node
- allow to select a node on empty node selection
https://github.com/user-attachments/assets/b00037a8-aa1d-4784-b973-05973649b46e
# Introduction
Adding view-group to core engine v2
Following https://github.com/twentyhq/twenty/pull/15010 ( same pattern )
## What's done
- Created flat-view-group
- flat view group runner
- flat view group builder
- create view group service v2 and input transpilers
- refactor the existing view group resolver to fix standard (
BREAKING_CHANGE on graphql api update especially ) REST stays the same
- refactored the front to consume the mutations autogenerated
close https://github.com/twentyhq/core-team-issues/issues/1665
To avoid huge workflows to block the worker, we will enqueue a new job
every 20 steps.
This could be more than 20 if there are branches but I think this is
fine, the goal is only to have a limit set.
Also cleaning a bit the code to mark running steps as failed when
workflow fails.
I tested it on a huge workflow:
https://github.com/user-attachments/assets/d7b8e345-d1a1-4467-96fd-92117b500120
# Introduction
After updating a `viewField` in a view the frontend receives a missmatch
metadata version.
That's because the `flatFieldMetadata` needs to be invalidated on a
viewField addition as it contains its primary key in its cache.
Before we would be checking updated flat entity maps, meaning that on a
view field update the flat field metadata maps would also get updated,
but we also invalidate the old v1 cache at the same time. Resuling in a
metadata version missmatch that's not really relevant for the gql schema
integrity
Now we only check if a object or field actions has been processed, if
yes increment the metadata version.
We should deco-relate the v1 object and fields cache from the metadata
version that should only serve as a "Please refresh browser state
because gql schema has mutated"
Closes https://github.com/twentyhq/core-team-issues/issues/1563.
We now have two inter-group orderBy criteria: one on the aggregate
values, the other on the dimension values.
Ex: I am grouping companies by city and querying the average number of
employees for each group. I can either order the groups by the city
name, or by the average number of employees, or by one then the other.
I could actually also order groups by an aggregated value that I did not
ask for (ex: average ARR), but I cannot order groups by a field value I
did not group records by (ex: country).
[See discord
discussion](https://discord.com/channels/1130383047699738754/1425438213555753050/1425438223097921659)
An example of query variables:
```
{
"groupBy": [
{
"createdAt": {
"granularity": "QUARTER_OF_THE_YEAR"
}
},
{"city": true}
],
"orderBy": [
{
"aggregate": {
"avgEmployees": "DescNullsLast"
}
},
{
"aggregate": {
"percentyEmptyEmployees": "DescNullsLast"
}
},
{
"city": "AscNullsLast"
},
{
"createdAt": {
"orderBy": "AscNullsLast",
"granularity": "QUARTER_OF_THE_YEAR",
}
}
]
}
```
The aggregate orderBy criteria had already been implemented, but I
updated the implementation to add an "aggregate" key to prefix them, in
order to avoid confusion between the two + for the schema generation not
to break (otherwise we would have an issue when generating
OrderByWithGroupByInput if a user has created a field that has the same
name as an aggregate field, such as avgEmployees).
# Introduction
This PR introduces a huge type refactor that will leverage dynamic intra
entity optimistic flat maps update in the future and also a more
granular cache invalidation enhancing performances
close https://github.com/twentyhq/core-team-issues/issues/1717
close https://github.com/twentyhq/core-team-issues/issues/1716
close https://github.com/twentyhq/core-team-issues/issues/1643
## What's done
### Comparators centralization
Comparator is now done through global configuration as const for each
metadata names
Thanks to
Note:
Definition of standard is evolving, standard is now scoped to an app.
Meaning that a manifest should be able to update its own standards
objects but on other app standards ones ? Each synchronizable entities
will have a standardOverrides ?
## Typing refactor
### `AllFlatEntityTypesByMetadataName`
**Single source of truth for the complete type ecosystem**, mapping each
metadata name to its entity types, flat entities, and migration actions:
```typescript
export type AllFlatEntityTypesByMetadataName = {
fieldMetadata: {
actions: { created: CreateFieldAction; updated: UpdateFieldAction; deleted: DeleteFieldAction; };
flatEntity: FlatFieldMetadata;
entity: FieldMetadataEntity;
};
objectMetadata: { /* ... */ };
// ... all 10 metadata types
};
```
### `ALL_METADATA_NAME_MANY_TO_ONE_RELATIONS`
**Explicitly declares database relationships** between entities with
compile-time validation:
```typescript
export const ALL_METADATA_NAME_MANY_TO_ONE_RELATIONS = {
viewField: {
view: 'viewId',
fieldMetadata: 'fieldMetadataId',
},
cronTrigger: {
serverlessFunction: 'serverlessFunctionId',
},
// ... all relations
} as const satisfies MetadataNameAndRelations;
```
### `ALL_FLAT_ENTITY_CONFIGURATION`
**Centralizes comparison and serialization logic** for each metadata
type:
```typescript
export const ALL_FLAT_ENTITY_CONFIGURATION = {
fieldMetadata: {
propertiesToCompare: ['name', 'type', 'label', 'defaultValue', /* ... */],
propertiesToStringify: ['options', 'settings', 'defaultValue'],
},
objectMetadata: {
propertiesToCompare: ['nameSingular', 'namePlural', 'isActive', /* ... */],
propertiesToStringify: [],
},
// ... all metadata types
} as const satisfies AllFlatEntityConfiguration;
```
## Combined Impact
These three configurations work together to create a **strongly-typed,
centrally-managed metadata system**:
1. **`AllFlatEntityTypesByMetadataName`** defines *what exists*
2. **`ALL_METADATA_NAME_MANY_TO_ONE_RELATIONS`** defines *how they
relate*
3. **`ALL_FLAT_ENTITY_CONFIGURATION`** defines *how to compare and
serialize them*
**Result:** Builders and validators become thin wrappers around
type-safe, configuration-driven logic instead of containing scattered,
error-prone manual implementations.
## What's next
### StandardOverrides standardization
Every metadata entity can be a standard one for a workspace if it's an
installed app, which means it might not expose the whole entity api to
be editable through an import dynamically
The standard overrides logic should not be applied to Fields and Objects
but to every entities
At the moment we have a logic of `EDITABLE_PROPERTIES` through the api,
and also `STANDARD_OVERRIDEDABLE_PROPERTIES`
This should be configuration centered like `propertiesToCompare` and
`propertiesToStringify`.
Scoping this PR to two last for the moment. As update dispatch to
standardOverrides could be considered as a side effect prefer waiting to
start the side effect refactor
### Granular Optimistic deprecation
With this new grain at runtime we will be able to add a flat entity and
dispatch its addition to related flat maps, so we don't have to describe
an optimistic method for each flat entity operations
See `addFlatEntityToFlatEntityAndRelatedEntityMapsOrThrow`
Note: Still in wip and included in this PR but about to create a new one
to integrate these utils and remove existing methods
### ValidateBuildAndRun dynamic args typed defintion
We should restrain the devxp to send expected flat maps entity as at
least from to or dependency as we now have the grain both a type lvl and
runtime to do so
It should not be possible in the devxp to forgot adding the views to the
v2 builder when passing the view field anymore ( that would lead to
permanent validation error in view field integrity checks )
## Conclusion
Thanks for reading and reviewing !
Any suggestions are more than welcomed ! ( same as for questions too ! )
Made the command more explicit for idempotency.
- Drop index explicitly (even though PostgreSQL does it automatically
when a column is dropped, but it might be good practice to account for
any unforeseen failures).
- Drop column.
- Create column.
- Create index (if we reach this point, column has already been created
and no error was thrown).
Figured we do not need extra queries to check ObjectMetadata for the
existence of person table on a workspace - we can use the IF EXISTS
syntax instead.
---------
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Avoid fetching full steps and trigger for versions that are not the
current version. Because those won't be used anyway. Better for
performances.
Only difficulty was for the `createDraft` mutation. I needed to return
the full created version so I can store it in cache and use it as new
`currentVersion`. Otherwise the current version is considered as
incomplete for a short time, since workflow is fetched separately from
the current version.
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
This big PR implements table virtualization with an offset paging,
allowing a way more fluid UX.
It is a v1 that should be improved in the future with partial data
loading and optimization of the browser display performance of a row.
But with this PR we have the solid enough technical foundation, both
frontend and backend, to get to a smooth table UX.
Fixes and improvements after first successful round of development
(needed to have main clean) :
- [x] Delete should refresh virtualized portion only and reset all table
- [x] Fix add new : top and bottom
- [x] Table empty shouldn’t show when first loading
- [x] Fix d&d
- [x] Fix sorts
- [x] Fix drag when scrolling after a full virtual page (it throws an
error)
- [x] Si update mais qu’on a un sort ou filter, alors il faut trigger le
refresh
- [x] Reset scroll position between tables
- [x] Reset scroll shadows between tables
- [x] Setup d&n for virtual list :
https://github.com/hello-pangea/dnd/blob/main/docs/patterns/virtual-lists.md
- [x] Full table re-render when entering edit mode
- [x] Clean code and prepare for merge
Fixes https://github.com/twentyhq/core-team-issues/issues/1613 that
contains other bugs to be fixed before merge
---------
Co-authored-by: Charles Bochet <charles@twenty.com>