Implements permission intersection (AND logic) to prevent permission
escalation when agents act on behalf of users.
### Changes:
- **Permission Intersection**: Operations requiring both user AND agent
permissions
- **RoleContext Type**: Unified type supporting single `roleId` or
multiple `roleIds` for intersection
- **CRUD Services**: Updated to accept `roleContext` for granular
permission control
- **Agent Integration**: Chat agents now use user + agent role
intersection for all operations
- **ORM Layer**: Enhanced `getRepository` to support multi-role
permission checks
### Related:
- Part 2 of ["Acting on behalf of user" concept
PR](https://github.com/twentyhq/twenty/pull/15103)
[Closes#1661](https://github.com/twentyhq/core-team-issues/issues/1661)
---------
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
## Problem
CI workflow started timing out on October 14, 2025 after commit
`d750df7fff` removed the trailing newline from `.env.example`.
## Root Cause
When `.env.example` lacks a trailing newline:
```bash
# Last line without newline
# CLICKHOUSE_URL=...twenty
```
And CI runs:
```bash
echo "NODE_PORT=3002" >> .env
```
Result:
```bash
# CLICKHOUSE_URL=...twentyNODE_PORT=3002 ← Commented out!
```
Server starts on default port 3000 instead of 3002, health check fails.
## Fix
1. **Restore trailing newline** to `.env.example`
2. **Make all CI `.env` operations robust** by adding `echo "" >> .env`
before appending
3. **Simplified `set_env_var`** function to always add newline first
Now works regardless of whether template files have trailing newlines.
## Files Changed
- 6 CI workflow files
- 1 .env.example file
## Summary
**Step 1 of 2:** Implements the "acting on behalf of user" concept for
workflows and agents to prevent permission escalation and maintain
proper audit trails.
## Problem
Previously, workflows and agents would bypass permissions regardless of
who initiated them, allowing users to escalate their privileges by
triggering workflows that performed actions they couldn't do directly.
## Solution
### For Workflows
Introduced `WorkflowExecutionContext` service that determines execution
mode:
- **Manual triggers/test button**: Uses user's roleId for permissions,
user's identity for `createdBy`
- **Automated triggers** (cron, database events, webhooks): Bypasses
permissions, uses workflow identity
### For Agents
**In Chat:**
- Always act on behalf of the user
- Use user's roleId for permission checks
- Use user's identity for `createdBy`
# Step 1 vs Step 2
### ✅ Step 1 (This PR): Acting on Behalf Concept
- Introduced `isActingOnBehalfOfUser` boolean concept
- Single roleId used for permission checks (user's OR system bypass)
- `createdBy` field properly attributes actions to initiator
- Prevents permission escalation in user-initiated flows
### 🔜 Step 2 (Future): Multi-Role Permission Support
- Support role intersection: `{ intersection: ['roleA', 'roleB'] }`
- Support role union: `{ union: ['roleA', 'roleB', 'roleC'] }`
- Enable user+agent collaboration scenarios
- Update `WorkspaceEntityManager` and `WorkspaceDatasource` to handle
multiple roleIds
---------
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
## 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>
## 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
# 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
# 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 ! )
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>
# Introduction
Migrating `viewFilter` to v2 in order to migrate later the field update
side effect on view to v2 too
## What's done
- Created flat-view-filter
- flat view filter runner
- flat view filter builder
- create view filter service v2 and input transpilers
- refactor the existing view filter resolver to fix standard (
BREAKING_CHANGE on graphql api update especially ) REST stays the same
- refactored the front to consume the mutations autogenerated
## New generic tools
### Compare two flat entity
Introducing a new util to compare two flat entity, it's strictly typed
and will be added to the generic builder in a following PR
This will ease flat entity addition as won't required to create a
specific abstraction for comparison
Generic builder will expect specific constant: properties to compare and
properties to stringify
### Transform flat entity for comparison
Forked and refactor the initial existing method for flat entity business
scope and type safety
## Coverage
Migrated existing integration tests to fit new contract API
This PR does not add strong coverage on validation exceptions
Deadlines are too short
close https://github.com/twentyhq/core-team-issues/issues/1666
# Introduction
Preparing view-filter and view-group introduction in v2 core engine
Moving view from `core-modules` to `metadata-modules`
## What happened
### Created dedicated modules for each view entity:
- ViewFieldModule
- ViewFilterModule
- ViewFilterGroupModule
- ViewGroupModule
- ViewSortModule
### Each module is now completely independent with its own:
- Controller
- Resolver
- Service
- Entity
### Created dedicated abstraction metadata module folder for:
- flat-view-field
- flat-view
### Dependencies
- Eleminated circular dep on ViewModule to all others ones
- Granular import not importing the whole viewModule anymore everywhere
close https://github.com/twentyhq/core-team-issues/issues/1703