- fixedOverflowWidgets fixes the suggestions being overflow outside the
editor
- on focus, stop event propagation to avoid catching spaces as document
level events
# Introduction
Following https://github.com/twentyhq/twenty/pull/17632 and
https://github.com/twentyhq/twenty/pull/17572
This PR deprecates the agent, skill, field metadata and role
`standardId` in favor of the `universalIdentifier` usage
## Note
- Removed previous standard ids declaration modules
- Twenty-sdk now re-exports the `STANDARD_OBJECTS` universalIdentifier
hashmap constant
- deleted some sync-metadata deadcode too ( mainly types )
Fixes https://github.com/twentyhq/twenty/issues/17544
**Problem**
When users create custom objects with short acronym names like "O&J",
the system generates an object name oJ. When creating relation fields,
the morph field name was built using string concatenation:
const morphFieldName = `target${capitalize("oJ")}`; // → "targetOJ"
This produced "targetOJ", which failed validation because the camelCase
check performed in `validateFlatFieldMetadataName` (camelCase(name) ===
name) returns "targetOj" for "targetOJ". The issue comes from
consecutive camelCase() operations.
**Solution**
Actually, the `camelCase(name) === name` check is questionnable.
What we want to check is that a name is in camelCase format, not that it
corresponds to the camelCase version of a given string, while that's we
are doing here. lodash does not provide camelCase validator, only
camelCase convertor, so we used it as a way to validate the format of
the name.
We may feel like `camelCase(name) === name` checks whether a name is
camel-cased, but in addition to that it is also checking for a camel
case "idempotency" we don't necessarily have and do not need: for
instance if an object's name is "iOS" (which could be inferred from a
label "I O S"), it won't pass the check: camelCase("iOS") is "ios" and
"ios" !== "iOS".
The existing check with
`STARTS_WITH_LOWER_CASE_AND_CONTAINS_ONLY_CAPS_AND_LOWER_LETTERS_AND_NUMBER_STRING_REGEX`
acts as a camel case validator, so we don't need that camelCase() check.
## Summary
- **Increase `--max-turns` from 50 to 200** — Claude was hitting the
turn limit before getting to `gh pr create`, resulting in branches with
no PR
- **Add common bash commands to `--allowedTools`** — `rm`, `find`,
`grep`, `cat`, `ls`, `head`, `tail`, `wc`, `sort`, `uniq`, `mkdir`,
`cp`, `mv`, `touch`, `chmod`, `echo`, `curl`, `cd`, `pwd`, `diff`,
`xargs`, `awk`, `cut`, `tee`, `tr` — denied commands were wasting turns
on retries
## Context
Both [run
21594509983](https://github.com/twentyhq/twenty/actions/runs/21594509983)
and [run
21595364188](https://github.com/twentyhq/twenty/actions/runs/21595364188)
failed with `error_max_turns` after exhausting 50 turns. Permission
denials on `rm` and `find` contributed to wasted turns.
## Test plan
- [ ] Tag `@claude` on an issue with a code change request — verify it
completes and creates a PR
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
## Summary
- Replace all `depot-ubuntu-24.04-8` runner references with the
equivalent GitHub-hosted `ubuntu-latest-8-cores` runner
- Updated across 4 workflow files: `ci-front.yaml`, `ci-server.yaml`,
`ci-emails.yaml`, `ci-sdk.yaml`
- Also updated cache key names in `ci-front.yaml` that referenced the
depot runner name
## Test plan
- [ ] Verify CI workflows run successfully on the new GitHub-hosted
larger runners
- [ ] Confirm cache keys work correctly with the updated naming
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
# Introduction
In this PR we're deprecating the object metadata standard id and
replacing it to the universalIdentifier usage
As we've totally removed its insertion for both new field and object in
https://github.com/twentyhq/twenty/pull/17572
## Note
- Removed upgrade commands before `1.17`
## Summary
- **Set `cancel-in-progress: false`** — Claude's own comments (e.g.
error reports) were triggering new workflow runs via `issue_comment`,
which cancelled the in-progress Claude run before the new run's `if`
condition could skip it. Disabling cancel-in-progress prevents this.
- **Filter out Bot users in `if` conditions** — Added
`github.event.comment.user.type != 'Bot'` so Claude's own comments don't
start job evaluation at all.
- **Add back `additional_permissions: actions: read`** — Lets Claude
read CI failure logs to help debug broken builds.
- **Remove `discussion_comment` trigger and job** — `claude-code-action`
doesn't support this event type yet (throws `Unsupported event type:
discussion_comment`). Listed as planned in their security.md.
## Context
Claude runs were consistently failing with `The operation was canceled`
because:
1. Claude posts an error/status comment back to the issue
2. That comment triggers a new `issue_comment` workflow run on the same
issue number
3. The concurrency group cancels the in-progress run before the new run
evaluates its `if` and skips
## Test plan
- [ ] Tag `@claude` on an issue — should run to completion without being
cancelled
- [ ] Verify Claude's own reply comments don't trigger new workflow runs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
## Summary
- `allowed_tools` is not a valid input for `claude-code-action@v1` — it
was silently ignored (visible as a warning in CI logs)
- This caused Claude to lack file write permissions, preventing it from
making actual code changes
- Move the tools list into `claude_args` as `--allowedTools` where it's
actually parsed
## Test plan
- [ ] Trigger `@claude` on a cross-repo issue requesting a code change —
verify it can edit files and create a PR
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
# Introduction
Important note: This PR officially deprecates the `standardId`, about to
drop col and entity property after this has been merged
Important note2: Haven't updated the optimistic tool to also update the
universal identifier aggregators only the ids one, they should not be
consumed in the runner context -> need to improve typing or either the
optimistic tooling
In this PR we're introducing all the devxp allowing future metadata
incremental universal migration -> this has an impact on all existing
metadata actions handler ( explaining its size )
This PR also introduce workspace agnostic create update actions runner
for both field and object metadata in order to battle test the described
above devxp
Noting that these two metadata are the most complex to handle
Notes:
- A workspace migration is now highly bind to a
`applicationUniversalIdentifier`. Though we don't strictly validate
application scope for the moment
## Next
Migrate both object and field builder to universal comparison
## Universal Actions vs Flat Actions Architecture
### Concept
The migration system uses a two-phase action model:
1. **Universal Actions** - Actions defined using `universalIdentifier`
(stable, portable identifiers like `standardId` + `applicationId`)
2. **Flat Actions** - Actions defined using database `entityId` (UUIDs
specific to a workspace)
### Why This Separation?
- **Universal actions are portable**: They can be serialized, stored,
and replayed across different workspaces
- **Flat actions are executable**: They contain the actual database IDs
needed to perform operations
- **Decoupling**: The builder produces universal actions; the runner
transpiles them to flat actions at execution time
### Transpiler Pattern
Each action handler must implement
`transpileUniversalActionToFlatAction()`:
```typescript
@Injectable()
export class CreateFieldActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'create',
'fieldMetadata',
) {
override async transpileUniversalActionToFlatAction(
context: WorkspaceMigrationActionRunnerArgs<UniversalCreateFieldAction>,
): Promise<FlatCreateFieldAction> {
// Resolve universal identifiers to database IDs
const flatObjectMetadata = findFlatEntityByUniversalIdentifierOrThrow({
flatEntityMaps: allFlatEntityMaps.flatObjectMetadataMaps,
universalIdentifier: action.objectMetadataUniversalIdentifier,
});
return {
type: action.type,
metadataName: action.metadataName,
objectMetadataId: flatObjectMetadata.id, // Resolved ID
flatFieldMetadatas: /* ... transpiled entities ... */,
};
}
}
```
### Action Handler Base Class
`BaseWorkspaceMigrationRunnerActionHandlerService<TActionType,
TMetadataName>` provides:
- **`transpileUniversalActionToFlatAction()`** - Abstract method each
handler must implement
- **`transpileUniversalDeleteActionToFlatDeleteAction()`** - Shared
helper for delete actions
## FlatEntityMaps custom properties
Introduced a `TWithCustomMapsProperties` generic parameter to control
whether custom indexing structures are included:
- **`false` (default)**: Returns `FlatEntityMaps<MetadataFlatEntity<T>>`
- used in builder/runner contexts
- **`true`**: Returns the full maps type with custom properties (e.g.,
`byUserWorkspaceIdAndFolderId`) - used in cache contexts
## Create Field Actions Refactor
Refactored create-field actions to support relation field pairs
bundling.
**Problem:** Relation fields (e.g., `Attachment.targetTask` ↔
`Task.attachments`) couldn't resolve each other's IDs during
transpilation because they were in separate actions with independent
`fieldIdByUniversalIdentifier` maps.
**Solution:**
- Removed `objectMetadataUniversalIdentifier` from
`UniversalCreateFieldAction` and `objectMetadataId` from
`FlatCreateFieldAction` - each field now carries its own
- Runner groups fields by object internally and processes each table
separately
- Split aggregator into two focused utilities:
- `aggregateNonRelationFieldsIntoObjectActions` - merges non-relation
fields into object actions
- `aggregateRelationFieldPairs` - bundles relation pairs with shared
`fieldIdByUniversalIdentifier`
Claude was blocked from writing files, using git/gh, and fetching
web content because only a few Bash patterns were pre-approved.
Add Edit, Write, WebFetch, git, gh, sed, and python3 to the
allowed_tools list.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The claude-code-action needs id-token: write to fetch an OIDC token.
The claude-cross-repo job was missing its permissions block entirely,
causing it to fail with "Could not fetch an OIDC token".
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
## Summary
- Move build/env/DB setup out of the GitHub Actions workflow into an
on-demand script (`packages/twenty-utils/setup-dev-env.sh`) that Claude
invokes only when needed
- Add Nx/build cache restore/save steps to speed up repeated runs
- Add `allowed_tools` so Claude can run the setup script and common dev
commands (nx, jest, yarn)
- Add `PG_DATABASE_URL` env var via `settings` for the postgres MCP
server
- Remove unnecessary `actions: read` permission grant
- Document the lazy setup pattern in CLAUDE.md
This makes read-only tasks (code review, architecture questions, docs)
fast by skipping the ~2min build/setup overhead, while still letting
Claude run tests and builds when it needs to.
## Test plan
- [ ] Comment `@claude what is the architecture of the backend?` —
should answer fast without running setup
- [ ] Comment `@claude run the twenty-server unit tests` — should call
setup script first, then run tests
- [ ] Verify cross-repo dispatch still works
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This PR handles SSE event stream edge cases.
- When a pod restarts, the front clients have to reconnect to SSE
- When the dev server restarts or is hot reloaded, the front client has
to reconnect to SSE
- When redis server restarts or the redis key is cleared for any reason,
the server has to recreate the event stream in redis, this can happen
when navigating for example.
- Log in / log out flow
With this PR we avoid error messages in the front end due to TTL or pod
crash, we implement a resilient way of reconnecting silently.
To avoid DDoSing our servers if pods crash or a full restart of the
cluster is made, we evenly space retry attempts to reconnect from all
the clients, to avoid n clients reconnection at the same time, we use a
random wait time between 0 and a constant max wait time (set to 2 mins
for now). This is the cheapest and most effective solution, clients who
want to force reconnect have to refresh or navigate to another page.
Fixes https://github.com/twentyhq/core-team-issues/issues/2045
## Problem
Favorite records (`NavigationMenuItems`) were not showing in the sidebar
under Favorites as the BE was returning `null` for
`targetRecordIdentifier`.
## Root cause
The `targetRecordIdentifier` resolver used `context.req`, which does not
have the typed `WorkspaceAuthContext` shape.
## Fix
Use `getWorkspaceAuthContext()` instead of `context.req` so the resolver
receives the typed auth context.
## Summary
- Add `--model opus` to `claude_args` in both the direct and cross-repo
Claude jobs
## Test plan
- [ ] Merge and trigger @claude — verify it reports running Opus 4.5
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
## Summary
- Add `repository_dispatch` trigger to `claude.yml` so `@claude`
mentions in `twentyhq/core-team-issues` get forwarded here
- New `claude-cross-repo` job: builds a prompt from the dispatch
payload, runs Claude Code against the twenty codebase, and posts a link
back to the source issue
- Switch both jobs to use `claude_code_oauth_token` instead of
`anthropic_api_key`
A companion workflow (`claude-dispatch.yml`) was pushed to
`twentyhq/core-team-issues` to send the dispatches.
## Test plan
- [x] Dispatch workflow in core-team-issues triggers successfully
(tested via issue #2194)
- [ ] After merge, verify `repository_dispatch` triggers the
`claude-cross-repo` job in twenty
- [ ] Verify Claude responds and posts a link back to the source issue
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
## 🤖 Installing Claude Code GitHub App
This PR adds a GitHub Actions workflow that enables Claude Code
integration in our repository.
### What is Claude Code?
[Claude Code](https://claude.com/claude-code) is an AI coding agent that
can help with:
- Bug fixes and improvements
- Documentation updates
- Implementing new features
- Code reviews and suggestions
- Writing tests
- And more!
### How it works
Once this PR is merged, we'll be able to interact with Claude by
mentioning @claude in a pull request or issue comment.
Once the workflow is triggered, Claude will analyze the comment and
surrounding context, and execute on the request in a GitHub action.
### Important Notes
- **This workflow won't take effect until this PR is merged**
- **@claude mentions won't work until after the merge is complete**
- The workflow runs automatically whenever Claude is mentioned in PR or
issue comments
- Claude gets access to the entire PR or issue context including files,
diffs, and previous comments
### Security
- Our Anthropic API key is securely stored as a GitHub Actions secret
- Only users with write access to the repository can trigger the
workflow
- All Claude runs are stored in the GitHub Actions run history
- Claude's default tools are limited to reading/writing files and
interacting with our repo by creating comments, branches, and commits.
- We can add more allowed tools by adding them to the workflow file
like:
```
allowed_tools: Bash(npm install),Bash(npm run build),Bash(npm run lint),Bash(npm run test)
```
There's more information in the [Claude Code action
repo](https://github.com/anthropics/claude-code-action).
After merging this PR, let's try mentioning @claude in a comment on any
PR to get started!
---------
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
## Summary
When a select option label is long, it was pushing the aggregate value
(e.g., '12.6m') out of view because the tag had `flex-shrink: 0`.
Changed to `min-width: 0` and `overflow: hidden` to allow the tag to
shrink and truncate with ellipsis, keeping the aggregate value visible.
## Changes
- Modified `StyledTag` in
[RecordBoardColumnHeader.tsx](cci:7://file:///root/74/bronze/twenty/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx:0:0-0:0)
to allow shrinking
The
[Tag](cci:1://file:///root/74/bronze/twenty/packages/twenty-ui/src/components/tag/Tag.tsx:100:0-141:2)
component already has built-in text truncation with
`OverflowingTextWithTooltip`, so this change enables that functionality
to work properly.
Fixes#17350
```**
---------
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
https://github.com/user-attachments/assets/c4e63edb-6e98-44bc-841f-ee110ae712d4
How it works
- `manifest.json` files are now committed when apps are published to our
repo
- to display available apps, from the server, we read into our github
repo, using a pod-scoped cache
- feature flagged
- app installation will be behind permission gate MARKETPLACE_APPS
Limitations and what is yet to develop
- content and settings tabs
- installed apps tab
- app installation
- test and potentially fix reading from .manifest.json once [Reshape
manifest
structure](https://github.com/twentyhq/core-team-issues/issues/2183) is
done. additional work is expected on assets notably. (couldnt properly
do it here as manifest.json will only be committed after this pr)
- we only read in community/ folder for now - we may want tochange that
- the cache is rather artisanal for now and scoped by pod - we may want
to change that
Fixes#17111
### Problem
When creating a Relation field, after deselecting the pre-selected
default object (e.g., Company) and selecting a different object (e.g.,
Opportunity), both objects would end up being selected, showing "2
Objects" instead of just the newly selected one.
### Root Cause
The `initialMorphRelationsObjectMetadataIds` array was being recreated
on every component render, causing react-hook-form's `Controller` to
treat the `defaultValue` prop as a new value and re-apply it after user
changes.
### Solution
1. **Memoized the initial value**: Used `useMemo` to ensure
`initialMorphRelationsObjectMetadataIds` has a stable reference across
renders
2. **Added initialization guard**: Used `useEffect` with a `useRef` flag
to ensure the default value is only set once during component mount
3. **Maintained Controller defaultValue**: Kept the `defaultValue` prop
on the Controller, which now works correctly with the stable memoized
value
### Changes
- `SettingsDataModelFieldRelationForm.tsx`: Added memoization and
initialization guard to prevent default value re-application
---------
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
Removes the versioning system for logic functions (`latestVersion`,
`publishedVersions`) and simplifies the file storage structure.
### Changes
- Remove `publishOneLogicFunctionOrFail` and publishing logic from
workflow status updates
- Add `createLogicFunctionFromExistingLogicFunction` to duplicate logic
functions when creating draft workflow versions
- Update `createDraftStep` to create a new logic function copy instead
of referencing the same one
- Migrate file storage to v2 endpoints with
`applicationUniversalIdentifier`
- Unify path structure: source files at `source/workflow/{id}/`, built
files at `built-logic-function/workflow/{id}/`
- Store full paths in `sourceHandlerPath` and `builtHandlerPath` entity
fields
## Summary
- Remove `latestVersion` and `publishedVersions` columns from
`LogicFunction` entity
- Remove version parameter from logic function execution, build, and
source code retrieval
- Update all related services, DTOs, utilities, and tests
- Add database migration to drop the version columns
# Introduction
In this PR we're migrating both the `field` and `object` metadata to be
using the new `FlatEntityFromV2` that requires all the
`UniversalFlatEntityExtraProperties` to be spread at the flat entity
root.
This means that we have to update all of their flat declaration
This type swap allows to isole a specific entity migration into his own
type scope and avoid to have everything handled at once
```ts
/**
* Currently under migration but aims to replace FlatEntity afterwards
*/
export type FlatEntityFromV2<
TEntity,
TMetadataName extends AllMetadataName | undefined = undefined,
TInnerFlatEntity extends { __universal?: unknown } = FlatEntityFrom<
TEntity,
TMetadataName
>,
> = Omit<TInnerFlatEntity, '__universal'> & TInnerFlatEntity['__universal'];
```
## Impact
Both object and field:
- Create input transpilation utils
- from entity to flat tools
- mocks
## Note
Removed from the universal extra properties the jsonb properties that do
not contain a serialized
## Next
Next step is to incrementally make the builder and runner expect
`UniversalFlatEntity` for both of these metadata
This way we will be able to fully migrate an entity e2e typesafely
React was warning that custom props like `isExpanded` and `allMatched`
were being passed to DOM elements (SVG icons and divs), which only
accept standard HTML/SVG attributes.
## Changes
Added `shouldForwardProp` configuration to styled components to filter
out custom props before they reach the DOM:
```typescript
import isPropValid from '@emotion/is-prop-valid';
const StyledChevronIcon = styled(IconChevronDown, {
shouldForwardProp: (prop) =>
isPropValid(prop) && prop !== 'isExpanded',
})<{ isExpanded: boolean }>`
transform: ${({ isExpanded }) => isExpanded ? 'rotate(180deg)' : 'rotate(0deg)'};
`;
```
This pattern is already used in other components (e.g.,
`NavigationDrawerItem`, `TableRow`) and ensures custom props are
available for styling logic but don't leak to the underlying DOM
element.
## Files Modified
- `FieldsWidgetSectionContainer.tsx` - `isExpanded` on icon component
- `UnmatchColumnBanner.tsx` - `isExpanded`, `allMatched` on icon, div,
and Banner components
<!-- START COPILOT CODING AGENT TIPS -->
---
💡 You can make Copilot smarter by setting up custom instructions,
customizing its development environment and configuring Model Context
Protocol (MCP) servers. Learn more [Copilot coding agent
tips](https://gh.io/copilot-coding-agent-tips) in the docs.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Devessier <29370468+Devessier@users.noreply.github.com>
Co-authored-by: Devessier <baptiste@devessier.fr>
**Fixes #17420**
### Problem
The billing/pricing display showed inconsistencies:
- "Get Started" / trial signup flow: simple `$9` (no decimals)
- Pricing page: `$9.00` (with decimals)
For yearly plans (with discount), the effective monthly price might not
always be an integer, leading to messy floats in UI (e.g., 7.5 showing
as 7.499999 or inconsistent formatting).
### Solution
Added a helper function `formatYearlyPriceIfNeccessary` that:
- Only applies to yearly subscriptions (`SubscriptionInterval.Year`)
- Calculates effective monthly price (`price / 12`)
- Returns an integer if the result is whole (`Number.isInteger`)
- Otherwise formats to exactly 2 decimal places (`toFixed(2)` → Number)
This ensures clean, consistent display:
- Whole numbers: no decimals (e.g., $9 → 9)
- Fractional: precise 2 decimals (e.g., $81 yearly → 6.75)
### Changes
- Added `formatYearlyPriceIfNeccessary` function (likely in a
utils/pricing file — specify the file path if you know it, e.g.
`src/utils/pricing.ts`)
- Applied it where yearly effective prices are rendered (e.g., in
BillingPlan component, Pricing page, Signup flow — list the
files/components you changed)
### Before / After
- Before (yearly example with discount): Might show 7.5 or 7.499999
- After: Always 7.5 (or 7 if integer)
### Testing
- Ran locally with `yarn dev`
- Checked signup flow and pricing page: prices now consistent and clean
- Verified non-yearly plans unchanged
Let me know if there's a preferred formatting style (e.g., always show
.00, or use Intl.NumberFormat) or if this should be applied in more
places!
Thanks for the great project, my first contribution btw
Closes#17420
---------
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
## Context
The previous WorkspaceAuthContext was a single interface with many
optional fields, making it unclear which fields are available in
different authentication scenarios. This made the code harder to reason
about and required runtime checks scattered throughout the codebase.
## Changes
- Introduced a discriminated union type for WorkspaceAuthContext with
four specific variants:
-> UserWorkspaceAuthContext - for authenticated users
-> ApiKeyWorkspaceAuthContext - for API key authentication
-> ApplicationWorkspaceAuthContext - for application-based auth
-> SystemWorkspaceAuthContext - for system/internal operations
- Added type guard functions (isUserAuthContext, isApiKeyAuthContext,
etc.) for safe type narrowing
- Added builder utilities (buildUserAuthContext, buildApiKeyAuthContext,
etc.) to construct each context variant with proper type safety
- Refactored WorkspaceAuthContextMiddleware to use the new builders
instead of constructing a loosely-typed object
- Moved the type definition from twenty-orm/interfaces/ to
core-modules/auth/types/ for better organization
- Updated all consumers across query runners, tool providers, and
modules to use the new type location
## Notes
- I had to query User and WorkspaceMember in some parts of tool module
that were expecting userWorkspaceId but not the rest of
UserWorkspaceAuthContext (that should be required with the new proper
type otherwise it would break a lot of logic and mostly permissions with
the newly added RLS -> This is what we expect from
UserWorkspaceAuthContext and how it's done in the "normal" path in HTTP
middleware)
- WorkspaceMember is in the cache already but ideally we should move
User (And Workspace?) in the cache as well to avoid querying the DB
after each request (this is also valid for HTTP middleware when we
hydrate the Request object btw)
Fixes: #16872
## Summary
Fixes the DateTimePicker to respect user's 12H/24H time format
preference. Previously, the time display at the top of the
DateTimePicker always showed 24-hour format (e.g., "14:30") regardless
of the user's time format setting.
## Screenshots
_After:_ Time respects user preference, showing 12H with AM/PM (e.g.,
"03:28 PM") or 24H format
<img width="357" height="491" alt="image"
src="https://github.com/user-attachments/assets/4a03f195-a52b-4f74-84ba-c5eb2fc6c8c1"
/>
## Implementation Details
### Changes Made:
1. **TimeMask.ts** - Added `getTimeMask()` to return appropriate mask
pattern based on time format
2. **TimeBlocks.ts** - Restored as constant file per linting
requirements
3. **getTimeBlocks.ts** (new) - Dynamic block generator supporting 12H
(1-12 + AM/PM) and 24H (0-23)
4. **DateTimeBlocks.ts** - Simplified to static constant
5. **getDateTimeMask.ts** - Updated to accept and use `timeFormat`
parameter
6. **DateTimePickerInput.tsx** - Dynamically generates mask and blocks
based on user's time format preference, increased input width to
accommodate AM/PM
7. **useParseJSDateToIMaskDateTimeInputString.ts** - Formats dates with
correct time pattern
8. **useParseDateTimeInputStringToJSDate.ts** - Parses dates with
correct time pattern
9. **date-utils.ts** - Added `getTimePattern()` helper (returns 'hh:mm
a' for 12H, 'HH:mm' for 24H)
10. **parseDateTimeToString.ts** - Added optional `timeFormat` parameter
for consistency
### Technical Approach:
- Leverages existing `useDateTimeFormat()` hook to get user's
`timeFormat` preference
- Supports `TimeFormat.SYSTEM`, `TimeFormat.HOUR_12`, and
`TimeFormat.HOUR_24`
- Uses IMask blocks with appropriate ranges: 1-12 for 12H, 0-23 for 24H
- Adds 'aa' (AM/PM) block for 12-hour format with enum validation
---------
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
Fix https://github.com/twentyhq/twenty/issues/17524
Recent update meant to support variable with spaces and dots broken the
database event variables. Inserting a variable `My Name` will be
inserted as `{{trigger.[My Name]}}`
For example, for a database event, we send `trigger.properties.after.id`
instead of `id`.
With the new code, we will consider `properties.after.id` as a variable
to wrap in `[]`, giving `trigger.[properties.after.id]` which cannot be
resolve.
Let's only support variable with spaces. Removing the logic to wrap
variable with dots.
# Introduction
In this PR we're refactoring the `FlatEntity` type to become a superset
of the `UniversalFlatEntity`.
Right now we're storing all the extra properties in `__universal`
property, at some point it might just be sibling to other entity and we
might rely on the `propertiesToCompare` constants and TypeScript
allowing passing a superset type into a smaller subset type
## FromTo utils
The entity to flat entity method now computes the universal information,
standardized a typing and pattern to do
## Example
Also strictly type
```ts
"bbb019ea-6205-498c-aea5-67bc53bce8a9": {
"workspaceId": "20202020-1c25-4d02-bf25-6aeccf7ea419",
"universalIdentifier": "20202020-d111-4d11-8d11-da5ab0a11002",
"applicationId": "d01b010d-b984-465b-b40b-370e954e5188",
"id": "bbb019ea-6205-498c-aea5-67bc53bce8a9",
"pageLayoutTabId": "791a512f-169f-4209-b731-aa86716668c6",
"title": "Deals by Company",
"type": "GRAPH",
"objectMetadataId": "9e14efea-df5b-4c0e-aba9-cfe455f32397",
"gridPosition": { "row": 0, "column": 6, "rowSpan": 6, "columnSpan": 6 },
"configuration": {
"color": "orange",
"orderBy": "FIELD_ASC",
"timezone": "UTC",
"displayLegend": true,
"displayDataLabel": false,
"showCenterMetric": true,
"configurationType": "PIE_CHART",
"firstDayOfTheWeek": 0,
"aggregateOperation": "COUNT",
"groupBySubFieldName": "name",
"groupByFieldMetadataId": "6673ff18-63d2-47a1-8f85-2b9b09ca27a5",
"aggregateFieldMetadataId": "8d64ee41-5dd4-4de6-945a-7c0c18399715"
},
"createdAt": "2026-01-28T14:08:52.140Z",
"updatedAt": "2026-01-28T14:08:52.140Z",
"deletedAt": null,
"__universal": {
"universalIdentifier": "20202020-d111-4d11-8d11-da5ab0a11002",
"applicationUniversalIdentifier": "20202020-64aa-4b6f-b003-9c74b97cee20",
"pageLayoutTabUniversalIdentifier": "20202020-d011-4d11-8d11-da5ab0a01001",
"objectMetadataUniversalIdentifier": "20202020-9549-49dd-b2b2-883999db8938",
"gridPosition": {
"row": 0,
"column": 6,
"rowSpan": 6,
"columnSpan": 6
},
"configuration": {
"color": "orange",
"orderBy": "FIELD_ASC",
"timezone": "UTC",
"displayLegend": true,
"displayDataLabel": false,
"showCenterMetric": true,
"configurationType": "PIE_CHART",
"firstDayOfTheWeek": 0,
"aggregateOperation": "COUNT",
"groupBySubFieldName": "name",
"aggregateFieldMetadataUniversalIdentifier": "20202020-d01a-4131-8a31-f123456789ab",
"groupByFieldMetadataUniversalIdentifier": "20202020-cbac-457e-b565-adece5fc815f"
}
}
},
```
## Summary
Fixes an issue where the AI chat would show loading shimmers
indefinitely when opened on a workspace with no conversation history.
**Root cause:** The `isLoading` state in `useAgentChat` included
`!currentAIChatThread`. On workspaces with no chat threads,
`currentAIChatThread` remained `null`, causing `isLoading` to be
permanently `true`.
**Changes:**
- Remove `!currentAIChatThread` from `isLoading` calculation in
`useAgentChat` - this state should only reflect streaming/file selection
status
- Auto-create a chat thread in `useAgentChatData` when the threads query
returns empty, ensuring a valid thread exists for the `useChat` hook to
initialize properly
- Add primary font color to empty state title for better visibility
## Test plan
1. Create a new workspace or use a workspace with no AI chat history
2. Open the AI chat
3. Verify the empty state shows (not infinite loading shimmer)
4. Send a message and verify it works correctly
Made with [Cursor](https://cursor.com)
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Fix https://github.com/twentyhq/twenty/issues/17507
The canvas layout mode is expected to render a single widget.
Previously, we threw an error when trying to render a canvas layout with
no widgets at all. This worked fine when we immediately rendered a
widget that used a single-widget canvas layout.
However, the active tab ID is shared across all page layouts with the
same ID, which doesn’t work well with conditional display. The available
tabs may depend on conditions such as the device displaying the page.
For example, when displaying a Note on the show page, the Note widget
appears on a separate tab. On mobile and in the side panel, however, it
is moved to the Home tab.
After that, if the user opens a Note in the side panel, the default
active tab might be the Note tab, which uses a _canvas_ layout mode and
would have no widgets at all when rendered in the side panel. This leads
to the error mentioned above.
The simple workaround is to wait one tick. An `Effect` component then
detects that the active tab doesn’t exist in the list of allowed tabs
and switches to another tab by default.
This feels more like a workaround than a proper solution, but I prefer
to fix it this way for now. We can later revisit this and design a
better separation for the selected tab ID state.
## Before
https://github.com/user-attachments/assets/500e332c-1623-48e5-b69b-5a4fa4e5e28d
## After
https://github.com/user-attachments/assets/b39dcf96-1852-42e4-a8cc-a79bf9036579
This PR adds what is required to handle soft-delete and restore SSE
events in virtualized table and board.
Restore is not handled in board for now as it requires respecting sorts
when inserting record ids.
Since virtualized table is refetching small chunks, we just refetch for
now.
The long term goal is to handle event handling without refetching in all
main components, and also handle SSE events that have the same origin
that the current tab. But for now we implement what is easily doable.
# QA
Delete between table and board (delete only) :
https://github.com/user-attachments/assets/715dd44a-007a-44ab-bf49-5ef039cd57c3
Delete and restore between table and table :
https://github.com/user-attachments/assets/f2122519-e969-491f-b71c-018d0f85bd86
## Summary
Migrates trigger entities (`CronTriggerEntity`,
`DatabaseEventTriggerEntity`, `RouteTriggerEntity`) into
`ServerlessFunctionEntity` by storing trigger settings as JSONB columns
directly on the serverless function. This simplifies the architecture
since these relationships were effectively one-to-one.
## Changes
### Schema Changes
- Added three new nullable JSONB columns to `ServerlessFunctionEntity`:
- `cronTriggerSettings` - stores cron pattern
- `databaseEventTriggerSettings` - stores event name and updated fields
filter
- `httpRouteTriggerSettings` - stores path, HTTP method, auth
requirements, and forwarded headers
### Core Logic Updates
- `CronTriggerCronJob` - now queries `ServerlessFunctionEntity` directly
instead of `CronTriggerEntity`
- `CallDatabaseEventTriggerJobsJob` - now queries
`ServerlessFunctionEntity` directly
- `RouteTriggerService` - now queries `ServerlessFunctionEntity`
directly
- `ApplicationSyncService` - extracts trigger settings from manifest and
writes to serverless function
## Context
Introduces a middleware that automatically sets the workspace auth
context in AsyncLocalStorage for HTTP requests, making it available
throughout the request lifecycle without explicit parameter passing.
The motivation behind this change is to reduce boilerplate and simplify
the developer experience when working with workspace data in HTTP
request handlers.
The Problem (Before)
Every HTTP request handler that needed to access workspace data had to:
- Extract auth-related info from decorators (@AuthWorkspace(),
@AuthUserWorkspaceId(), etc.) in controller/resolver and pass down to
services
- Build or pass the authContext explicitly (sometimes with type
assertion which was flaky)
Then call executeInWorkspaceContext(authContext, async () => { ... })
## Changes
- Add WorkspaceAuthContextMiddleware that extracts auth context from the
request and stores it in AsyncLocalStorage
- Register middleware for GraphQL, metadata, and REST routes (runs after
hydration middlewares)
- Simplify executeInWorkspaceContext signature: fn is now the first
parameter, authContext is optional second
- If authContext is not provided, it's automatically retrieved from the
storage (set by middleware)
- Update all callers (~120 files) to use the new parameter order
- Fixes a bug in search where system auth context was used, bypassing
RLS feature.
This PR reverts the change that was made to turn `Date` objects into ISO
string, because right now there are too many places in the code that use
both, either Date or ISO string from an entity returned by the querying
layer.
Unifying everything with ISO string is clearly the long term goal, but
right now it is too difficult, we should first refactor each part to ISO
string, then at the end remove Date from the codebase.
This PR fixes a bug that arises in `formatResult` following up the
recent refactor for DATE_TIME :
https://github.com/twentyhq/twenty/pull/17407
In the case of workflows, we pass a plain object to `formatResult` :
```ts
{
before: null,
after: '2026-01-27T10:59:15.525Z'
}
```
So a case has been added to handle this.
@Weiko are we ok with this more restrictive else-if part in this util ?
We could also just put a `continue` for unknown shapes.
Added example output showing what the roles table should look like after
creating the postgres role
Co-authored-by: Tom Larson <tomlarson@Toms-MacBook-Pro.local>
## Implement Navigation Menu Items Frontend
Implements the frontend for navigation menu items, the new system
replacing favorites.
### Changes
- Added GraphQL fragments and queries for navigation menu items
- Added hooks for managing navigation menu items (create, update,
delete, sorting, filtering)
- Updated components to use navigation menu items instead of favorites
- Added test coverage for utility functions
### Migration Note
The favorites and navigation menu item modules currently exist in
parallel. The favorites code will be removed once all data has been
migrated to navigation menu items.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> Replaces Favorites with feature-flagged `NavigationMenuItem` across
frontend and backend, while keeping Favorites as fallback until
migration completes.
>
> - UI: new `navigation-menu-item` components (folders, orphan items,
drag provider/droppable, icons, skeleton), dispatcher components to
switch from Favorites, and updated “Add to favorites” action to create
`NavigationMenuItem` when `IS_NAVIGATION_MENU_ITEM_ENABLED`
> - DnD: shared `validateAndExtractFolderId` and droppable id utils
moved to `ui/layout/draggable-list`; favorites DnD updated to use shared
utils
> - GraphQL (client): add fragments, queries, mutations, hooks
(create/update/delete/find), and generated types; added
`RecordIdentifier` and `targetRecordIdentifier` on `NavigationMenuItem`
> - Prefetch: new prefetch state/effect for navigation menu items; skip
favorites prefetch when flag enabled
> - Backend: add DTOs (`NavigationMenuItem`, `RecordIdentifier`),
resolver `targetRecordIdentifier` field, service logic to fetch record
identifiers with permission-aware access and image signing,
`getRecordImageIdentifier` util, entity relation to `view`, and
migration adding FK on `viewId`
> - Feature flags & seeding: add `IS_NAVIGATION_MENU_ITEM_ENABLED` to
enums, dev seeder enables it; standard app seeds workspace navigation
menu items instead of favorites when flag on
> - Tests: add unit tests for sorting/labels/folder id and related utils
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
c99746f08b. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Aman Raj <92664006+araj00@users.noreply.github.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Paul Rastoin <45004772+prastoin@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@twenty.com>
## Today
CTA is confusing
<img width="1442" height="947" alt="image"
src="https://github.com/user-attachments/assets/d5dd6229-e78d-4068-acbc-b5766b725b14"
/>
## Summary
- Replace the "Add account" button text with "Finish Setup" at the end
of the calendar account configuration flow
- Change the button icon from a plus sign (`IconPlus`) to a floppy disk
(`IconDeviceFloppy`) to better reflect saving/completing the setup
This PR fixes the inconsistency between Calendar and Table views when
trying to edit createdAt date field.
Previously, calendar cards could be dragged even though the createdAt
field are UI read-only, resulting in the confusing and inconsistent
behavior compared to the Table view where the field is not editable.
The issue was caused by the drag logic checking only user permissions
and not the field’s UI read-only metadata. This change updates the
calendar drag logic to also check for isUIReadOnly, ensuring that
records are not draggable when the selected calendar date field is
read-only.
---------
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
# Introduction
Previously the command would have been `old` renaming only any `owner`
field on `opportunity` object
Now we also search for any colliding `joinColumnName` with `ownerId`
which is also introduced by the new standard owner field
In a nutshell: previously gracefully handling existing custom `owner`
field collision but not for RELATION types that has an additional
collision surface: `joinColumnName`
Related
https://github.com/twentyhq/twenty/issues/17413#issuecomment-3799872079
Fixes issue where focusing the record title input would reset the title
to empty. This ensures the draft value is properly initialized from the
field value if it's undefined or empty when the input is focused.
Fixes#17437
---------
Co-authored-by: Daniel <daniel@example.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
# Introduction
In this PR we're introducing mainly two branded type signatures for both
`JsonbProperty` entities properties and `SerializedRelation` (jsonb
serialized property storing another entity id).
Allowing to dynamically map over them later in order to build universal
`jsonb` `serialized` relations.
## `JsonbProperty`
A branded wrapper type that marks entity properties stored as PostgreSQL
JSONB columns. It adds a phantom brand `__JsonbPropertyBrand__` to
object types while leaving primitives unchanged. The branded key is
optional and typed as never, also omitted when transpiled to
`UniversalFlat`
**Should be used at entities lvl only:**
```typescript
@Column({ type: 'jsonb', nullable: false })
gridPosition: JsonbProperty<GridPosition>;
@Column({ nullable: false, type: 'jsonb', default: [] })
publishedVersions: JsonbProperty<string[]>;
```
## `SerializedRelation`
A branded string type that marks foreign key IDs stored inside JSONB
objects. These are entity references serialized within a JSONB column
rather than being a regular database foreign key.
**Usage in jsonb property generic***
```ts
type FieldMetadataRelationSettings = {
relationType: RelationType;
onDelete?: RelationOnDeleteAction;
joinColumnName?: string | null;
junctionTargetFieldId?: SerializedRelation;
};
```
## `FormatJsonbSerializedRelation<T>`
A transformation type that processes JSONB properties for universal
entity mapping. It:
1. Detects properties with the `JsonbProperty` brand
2. Finds `SerializedRelation` properties
3. Renames them from `*Id` to `*UniversalIdentifier`
4. Removes the brand from the output type ( optional though )
```typescript
// Input: JsonbProperty<{ targetFieldMetadataId: SerializedRelation }>
// Output: { targetFieldMetadataUniversalIdentifier: SerializedRelation }
```
## Result
An example of the dynamic type mapping, through a type-test example
```ts
type SettingsTestCase = UniversalFlatFieldMetadata<
| FieldMetadataType.RELATION
| FieldMetadataType.NUMBER
| FieldMetadataType.TEXT
>['settings']
type SettingsExpectedResult =
| {
relationType: RelationType;
onDelete?: RelationOnDeleteAction | undefined;
joinColumnName?: string | null | undefined;
junctionTargetFieldUniversalIdentifier?: SerializedRelation | undefined;
}
| {
dataType?: NumberDataType | undefined;
decimals?: number | undefined;
type?: FieldNumberVariant | undefined;
}
| {
displayedMaxRows?: number | undefined;
}
| null;
type Assertions = [
Expect<Equal<SettingsTestCase, SettingsExpectedResult>>,
]
```
## Remarks
- Removed duplicated twenty-server and twenty-shared typed
- Removed class validator instances for default value that were not used
at runtime, we will refactor that to add validation across all entities
following a same pattern
# Introduction
Currently refactoring `flatEntity` typing, encountering some tsc errors
due to this fk aggregator custom override
It shall now follow generic pattern leading to be named `fieldIds`
Needs to flush object cache when released
fixes#16340
we are updating the cache partially to prevent the flow of the corrupted
fields into the cache
the fields get corrupted during the mutation process potentially
overriding the recoil cache as we are passing the `newRecordCache`
directly in `upsertRecordsInStore`, the newRecordCache is thin and hence
the recoil wipes out the fields that are undefined or empty
we prevent this by extracting the partial data ( only the data which is
being updated in the field ) and passing it to `upsertRecordsInStore`,
as it only touched the specific fields and updates the data, leaving the
other fields untouched.
https://github.com/user-attachments/assets/5256bef7-70c3-47b3-b2ce-dd02ee1a2de8
---------
Co-authored-by: Arun kumar <arunkumar@Aruns-MacBook-Air.local>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
# Introduction
A command allowing to invalidate flat cache entries. Extends the active
or suspended workspace coverage.
## Args
### --metadataName
If provided will invalidate metadata and related metadata cache entry (
can be repeated see usage )
### --all-metadata
Will invalidate all metadata entries
## Usage example
```
npx nx command twenty-server cache:flat-cache-invalidate --metadataName viewFilter --metadataName objectMetadata
```
```
npx nx command twenty-server cache:flat-cache-invalidate --all-metadata -w 0000-0000-0000-0000
```
## Summary
- **Fix junction relation toggle not being saved**: The form schema
wasn't tracking the `settings` field, so changes to
`junctionTargetFieldId` weren't marked as dirty
- **Add type-safe documentation paths**: Generate TypeScript constants
from `base-structure.json` to prevent broken documentation links
- **Create many-to-many relations documentation**: Step-by-step guide
for building many-to-many relations using junction objects
- **Update `getDocumentationUrl`**: Now uses shared constants from
`twenty-shared` for base URL, default path, and supported languages
## Key Changes
### Junction Toggle Fix
- Added `settings` field to the form schema in
`SettingsDataModelFieldRelationForm.tsx`
- Fixed the toggle to properly merge settings when updating
`junctionTargetFieldId`
### Type-Safe Documentation Paths
- New constants in `twenty-shared/constants`:
- `DOCUMENTATION_PATHS` - All 161 documentation paths as typed constants
- `DOCUMENTATION_SUPPORTED_LANGUAGES` - 14 supported languages
- `DOCUMENTATION_BASE_URL` / `DOCUMENTATION_DEFAULT_PATH`
- Generator script: `yarn docs:generate-paths`
- CI integration: Added to `docs-i18n-pull.yaml` workflow
### Documentation
- New article:
`/user-guide/data-model/how-tos/create-many-to-many-relations`
- Updated `/user-guide/data-model/capabilities/relation-fields.mdx` with
Lab warning and link
## Test plan
- [ ] Verify junction toggle saves correctly when enabled/disabled
- [ ] Verify documentation link opens correct localized page
- [ ] Verify `yarn docs:generate-paths` regenerates paths correctly
This fixes the #15896. For problem statement. Please refer to the shared
video mentioned in the issue.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> Aligns filter defaults across UI by centralizing initial value
computation.
>
> - Add boolean handling in `useGetInitialFilterValue` to return
`value/displayValue` of `'false'` when operand is `IS`
> - Update `WorkflowDropdownStepOutputItems` to use
`getInitialFilterValue` for initial `value` instead of an empty string,
based on field type and default operand
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
3ce05fa31a. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
Closes [1744](https://github.com/twentyhq/core-team-issues/issues/1744).
This PR migrates attachments to morph relations behind a feature flag,
following the TimelineActivity pattern. it introduces the
`IS_ATTACHMENT_MIGRATED` flag, updates standard field metadata and
indexes to use morph relations, adds a workspace migration that renames
`attachment.*Id` columns to `target*Id` and converts the corresponding
field metadata to `MORPH_RELATION` with a shared `morphId`. On the
frontend, attachment read/write paths now switch to `target*Id` when the
flag is enabled.
It also fixes optimistic filtering for morph join columns. The metadata
API deduplicates morph fields, so attachments now expose a single target
field of type `MORPH_RELATION` plus a `morphRelations` array listing
each target object. Because only one `settings.joinColumnName` is
returned (e.g. `targetRocketId`), filters like `targetCompanyId` don’t
map to any field and the optimistic cache code throws.
`doesMorphRelationJoinColumnMatch` resolves this by computing all valid
join column names from `morphRelations` using
`computeMorphRelationFieldName` and comparing them to the filter key.
That makes filters like `targetCompanyId` resolvable even with a single
target field, so attachment uploads and list matching no longer crash.
<img width="477" height="474" alt="image"
src="https://github.com/user-attachments/assets/50e19418-3438-4d1e-9f1f-1bc1a03174a9"
/>
<br />
<br />
Today the metadata API returns one morph field called `target` and a
list of possible targets (`morphRelations`), but it does not tell us the
join column for each target. That’s why the Frontend had to compute join
column names.
If we want to fix this at the API level, there are two options:
- Add join column names to each target in `morphRelations` (e.g. company
→ `targetCompanyId`). This is additive and low‑risk.
- Return each target as its own field (`targetCompany`, `targetPerson`,
etc.) instead of a single target. This is a larger change because it
changes the shape of metadata and would require more UI updates.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> Introduces morph relations for attachments behind
`IS_ATTACHMENT_MIGRATED`, aligning server schema/metadata and frontend
behavior.
>
> - Adds `IS_ATTACHMENT_MIGRATED` flag (frontend/server) and
seeds/defaults; updates generated GraphQL enums
> - New workspace upgrade `1.17` command migrates data: renames
`attachment.*Id` → `target*Id` and converts related fields to
`MORPH_RELATION` with shared `morphId`
> - Updates standard field metadata and indexes to `target*` (attachment
+ related objects), dev seeds, snapshots, and workspace entity types
> - Frontend: switches attachment read/write filters via
`getActivityTargetObjectFieldIdName` using the feature flag; updates
hooks/components (`useAttachments`, `useUploadAttachmentFile`, editors);
expands `Attachment` type
> - Fixes optimistic cache filtering to recognize morph join columns in
`isRecordMatchingFilter` by computing valid join-column keys from
`morphRelations`
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
f208fa23b1. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Félix Malfait <felix@twenty.com>
### Summary
The style guide currently discourages type imports, which contradicts
the enforced ESLint rule
`@typescript-eslint/consistent-type-imports` and existing code usage
(e.g. Storybook stories).
This PR updates the documentation to recommend inline type imports,
aligning the style guide
with tooling and preventing contributor confusion.
### Why
- Aligns documentation with enforced ESLint configuration
- Prevents ESLint auto-fix conflicts
- Improves contributor experience
Fixes #<### Summary
The style guide currently discourages type imports, which contradicts
the enforced ESLint rule
`@typescript-eslint/consistent-type-imports` and existing code usage
(e.g. Storybook stories).
This PR updates the documentation to recommend inline type imports,
aligning the style guide
with tooling and preventing contributor confusion.
### Why
- Aligns documentation with enforced ESLint configuration
- Prevents ESLint auto-fix conflicts
- Improves contributor experience
Fixes #<#17222>
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> Updates documentation to match tooling and current usage.
>
> - Replaces "Enforcing No-Type Imports" with **Type Imports**,
recommending inline type imports
> - Updates examples to prefer `import { type X } from ...` and avoid
importing types as runtime values
> - Clarifies ESLint `@typescript-eslint/consistent-type-imports`
configuration to prefer explicit type imports and enforce
`inline-type-imports` fix style
> - Changes confined to
`packages/twenty-docs/l/fr/developers/contribute/capabilities/frontend-development/style-guide.mdx`
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
e2250e43ad. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Fixes#16388
Now we have used the metadata fetching from network using
resetVirtualizationBecauseDataChanged(), as we navigate back to the
table after updating the field content. As the virtualised table uses
the "cache-first" strategy, it fails to give fresh data from the data
base
Please let me know i we need to add the loading state (and it's UI
requirements) while it fetches the cell content, as currently the cell
is empty until it's populated by fresh data
https://github.com/user-attachments/assets/901085ba-4337-4a5d-86c8-15ac84250e8f
---------
Co-authored-by: Arun kumar <arunkumar@Aruns-MacBook-Air.local>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
## Summary
- The `createMany` API was returning records in arbitrary order because
`fetchUpsertedRecords` used `WHERE id IN (...)` without `ORDER BY`
- This caused flaky tests that assumed input order was preserved (e.g.,
`people-merge-many.integration-spec.ts`)
- Fixed by reordering the fetched records to match the original input
order from `generatedMaps`
## Root Cause
```typescript
// Before: No ORDER BY, so SQL returns in arbitrary order
const upsertedRecords = await queryBuilder
.where({ id: In(objectRecords.generatedMaps.map((record) => record.id)) })
.getMany(); // Returns in arbitrary order!
```
## Fix
```typescript
// After: Reorder results to match original input order
const orderedIds = objectRecords.generatedMaps.map((record) => record.id);
const upsertedRecords = await queryBuilder
.where({ id: In(orderedIds) })
.getMany();
// Preserve original input order
const recordsById = new Map(upsertedRecords.map((record) => [record.id, record]));
return orderedIds
.map((id) => recordsById.get(id))
.filter((record) => record !== undefined);
```
## Test plan
- [x] Run `people-merge-many.integration-spec.ts` multiple times -
passes consistently
- [x] Lint passes
## Summary
- Adds SSRF (Server-Side Request Forgery) protection to webhook requests
by using the same secure axios adapter already used by HTTP workflow
actions
- Prevents webhooks from making requests to private/internal IP
addresses (10.x, 192.168.x, 172.16-31.x, 169.254.x, localhost)
- Adds specific error logging when a webhook fails due to SSRF
protection
## Context
The HTTP workflow tool (`HTTP_REQUEST` action) already had SSRF
protection via `HTTP_TOOL_SAFE_MODE_ENABLED`, but webhooks were using
`HttpService` directly without this protection. This inconsistency meant
users could potentially configure webhooks to probe internal
infrastructure.
### What's protected now:
| Feature | Before | After |
|---------|--------|-------|
| HTTP Workflow Action | Protected (secure adapter) | Protected (secure
adapter) |
| Webhooks | **Unprotected** | Protected (secure adapter) |
### The secure adapter validates:
1. Protocol must be `http:` or `https:`
2. DNS resolution of hostname
3. Resolved IP must not be in private ranges
## Test plan
- [ ] Configure a webhook with an external URL (e.g.,
`https://webhook.site`) - should work
- [ ] Configure a webhook with `http://localhost:3000` - should fail
with SSRF error in audit log
- [ ] Configure a webhook with `http://10.0.0.1/test` - should fail with
SSRF error in audit log
- [ ] Configure a webhook with a domain that resolves to a private IP -
should fail
This PR modifies our broadly used `formatResult` util to counter act
TypeORM transforming any date time to a `Date` object.
Instead we return the ISO string for any `DATE_TIME`, this way we're not
transporting Date object from one function to another in the backend.
We do this because there was problems working with events utils that
take string date time in parameters and received Date objects.
As this is a recurring problem and because it's an opinionated choice
from TypeORM, we chose to switch to string only in our codebase, from
TypeORM's output to frontend.
This PR changes the shape and logic of SSE events `DELETE` and
`RESTORE`, because they behave like `UPDATE` events in practice, they
should share the same logic.
Before this PR, it was impossible for the frontend to obtain the
`deletedAt` value, and the logic to handle soft-delete and restore would
have been flawed.
Because there is a typing confusion in the parameters of
`formatTwentyOrmEventToDatabaseBatchEvent`, due to TypeORM, we also
update this util to only accept an array of records, instead of `T |
T[]`. We should improve our TypeORM layer in the future.
Also the naming was not clear, so we clearly use `recordsAfter` and
`recordsBefore` as much as possible, because that is what we have at the
end in events.
Events are sent from their respective query builders, so these last ones
have been updated also.
Because TypeORM `soft-remove` operation only returns record ids, we add
`.getMany()` to fetch all fields for soft-removed records, so that our
event can have before and after.
## Summary
This PR fixes the `tsconfig` setup in `twenty-front` so that `tsgo -p
tsconfig.json` properly type-checks all files.
### Root Cause
The previous setup used TypeScript project references with `files: []`
in the main `tsconfig.json`. When running `tsgo -p tsconfig.json`, this
checks nothing because `tsgo` requires the `-b` (build) flag for project
references, but the configs weren't set up for composite mode.
### Changes
**Simplified tsconfig architecture (4 files → 2):**
- `tsconfig.json` - All files (dev, tests, stories) for
typecheck/IDE/lint
- `tsconfig.build.json` - Production files only (excludes tests/stories)
**Removed redundant configs:**
- `tsconfig.dev.json`
- `tsconfig.spec.json`
- `tsconfig.storybook.json`
**Updated references:**
- `jest.config.mjs` → uses `tsconfig.json`
- `eslint.config.mjs` → uses `tsconfig.json`
- `vite.config.ts` → uses `tsconfig.json` for dev
**Type fixes (pre-existing errors revealed by proper typechecking):**
- Made `applicationId` optional in `FieldMetadataItem` and
`ObjectMetadataItem`
- Added missing `navigationMenuItem` translation
- Added `objectLabelSingular` to Search GraphQL query
- Fixed `sortMorphItems.test.ts` mock data
## Test plan
- [ ] Run `npx nx typecheck twenty-front` - should pass
- [ ] Run `npx nx lint twenty-front` - should work
- [ ] Run `npx nx test twenty-front` - should work
- [ ] Run `npx nx build twenty-front` - should work
- [ ] Verify IDE type checking works correctly
# Introduction
Creating a `UniversalFlatEntityFrom` that strips out all the relation
and foreignKey properties in order to replace them with
`UniversalIdentifier` suffix
This data type will be major for the workspace migration workspace
agnostic refactor
## Chore
- renamed `flat-entity.type` to `flat-entity-from.type.ts` ( more
accurate to exported module )
- create static test type over the field metadata entity on quite
complex utils as both coverage and documentation
## Example
Here's an example of a `UniversalFlatEntityFrom<FieldMetadataEntity>`
```ts
const universalFlatFieldMetadata: UniversalFlatFieldMetadata<FieldMetadataType.RELATION> = {
// Base properties (from FieldMetadataEntity, excluding relations and applicationId)
universalIdentifier: '550e8400-e29b-41d4-a716-446655440001',
applicationUniversalIdentifier: '5800681c-088e-4e2b-9fc3-bcf6e8ec2051',
type: FieldMetadataType.RELATION,
name: 'firstName',
label: 'First Name',
defaultValue: null,
description: 'The first name of the person',
icon: 'IconUser',
standardOverrides: null,
options: null,
settings: {
relationType: RelationType.ONE_TO_MANY,
},
isCustom: false,
isActive: true,
isSystem: false,
isUIReadOnly: false,
isNullable: true,
isUnique: false,
isLabelSyncedWithName: true,
morphId: null,
// Date properties cast to string
createdAt: '2024-01-15T10:30:00.000Z',
updatedAt: '2024-01-15T10:30:00.000Z',
// ManyToOne relation universal identifiers (from FieldMetadataEntity relations)
relationTargetFieldMetadataUniversalIdentifier:
'550e8400-e29b-41d4-a716-446655440012',
relationTargetObjectMetadataUniversalIdentifier:
'550e8400-e29b-41d4-a716-446655440013',
// Join column universal identifiers (foreignKey -> universalIdentifier)
objectMetadataUniversalIdentifier: '550e8400-e29b-41d4-a716-446655440010',
// OneToMany relation universal identifiers (array of related entity identifiers)
viewFieldUniversalIdentifiers: [
'550e8400-e29b-41d4-a716-446655440020',
'550e8400-e29b-41d4-a716-446655440021',
],
viewFilterUniversalIdentifiers: ['550e8400-e29b-41d4-a716-446655440030'],
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
};
```
## Settings
Will hop on the settings typing next. Might not be dynamic but
declarative though
## Refactor app-dev state management and build utilities
- Store only `manifest` in `AppDevState` instead of full
`ManifestBuildResult`; add `sourcePath` to `FileStatus`
- Pass `sourcePaths` directly to watchers instead of
`ManifestBuildResult`
- Only reset `fileUploadStatus` for functions/components when their
source paths change
- Add pure `updateManifestChecksum` utility that returns a new manifest
without side effects
- Extract `processEsbuildResult` to deduplicate build result processing
between watchers
- Rename `serverlessFunctions` → `functions` in SDK code (API unchanged)
- Extract `writeManifestToOutput` to shared `manifest-writer.ts`
Until now, it wasn’t possible to navigate between page layouts.
Dashboards don’t contain links to other dashboards. With record page
layouts, however, a page layout can now contain a link to another page
layout. If the user opens a record in the side panel and then clicks a
link to another record, the current page layout is replaced with the
page layout for the clicked record.
In this scenario, the `PageLayoutRenderer` component is not remounted.
As a result, the `isInitialized` state was not reset to `false`, and the
corresponding initialization `useEffect` was not triggered again.
All page layout states are component states bound to
`PageLayoutComponentInstanceContext`. For example, after navigating to
another record, `pageLayoutPersistedComponentState` was actually
`undefined` because the context's `instanceId` had changed.
Replacing the `isInitialized` state with a component state fixes the bug
and is consistent with the existing pattern of using component states to
store everything related to page layouts.
## Before
https://github.com/user-attachments/assets/24217145-51e5-49ef-8180-de62a6acfe10
## After
https://github.com/user-attachments/assets/e0ffe107-8fae-4f3a-9016-72c85bc735f4
Previously we introduced useUpdateOneRecordV2 to handle morph relations.
In this PR we are removing it to only use useUpdateOneRecord which has
been adapted not to requires objectMetadataNameSingular in its props.
Fixes
https://github.com/twentyhq/private-issues/issues/410#issuecomment-3781085655
Currently, JSON keys with spaces like { "toto toto": 123 } are rejected
with "JSON keys cannot contain spaces" error. This is problematic for
HTTP requests and webhook triggers where users cannot control the
response structure.
We use Handlebars to eval variables, segment-literal bracket notation to
escape keys with special characters:
Normal: {{step.normalKey}}
With spaces: `{{step.[key with space]}}`
So we simply need to wrap segments with spaces with brackets.
This PR:
- Create shared path utilities to wrap the variable segments when needed
- Use it in all variable generation places
- Remove the restrictions
This body is now supported:
<img width="609" height="457" alt="Capture d’écran 2026-01-22 à 16 10
13"
src="https://github.com/user-attachments/assets/e7653c0a-df1e-49af-9c9a-4b7d59a99726"
/>
## Context
Removing full CRUD to RLS since we are actually using an upsert only
over the role resolver, this removes a lot of unused code (could be
re-added later but unlikely). Simplified the folder arch at the same
time
Add simple integration tests to RLS
**Summary**
Issue #16963: When an object is renamed, the morph relation picker still
shows the old name.
**Root cause**
The picker used searchRecord.objectNameSingular from the GraphQL search
response, which can be stale after a rename.
The search record stores the object name at query time, not the current
metadata.
**Solution**
- Updated SingleRecordPickerMenuItem:
- Added useObjectMetadataItems to access current object metadata.
- Look up the current object metadata by morphItem.objectMetadataId.
- Use labelSingular (or nameSingular as fallback) instead of
searchRecord.objectNameSingular for display.
- Updated MultipleRecordPickerMenuItemContent:
- Use objectMetadataItem.labelSingular (already available as a prop)
instead of searchRecord.objectNameSingular.
---------
Co-authored-by: Marie Stoppa <marie.stoppa@essec.edu>
# Introduction
In this PR we catch all the runner errors coming from a single workspace
migration action execution.
**1. `WorkspaceMigrationActionExecutionException`** (low-level,
action-specific)
- Thrown from action handlers, utils, and helper functions
- Contains specific error codes: `FIELD_METADATA_NOT_FOUND`,
`OBJECT_METADATA_NOT_FOUND`, `ENUM_OPERATION_FAILED`, `NOT_SUPPORTED`,
etc.
- Simple structure: `message`, `code`, `userFriendlyMessage`
- No action context - just describes what went wrong
**2. `WorkspaceMigrationRunnerException`** (high-level, runner-scoped)
- Only two codes: `INTERNAL_SERVER_ERROR` and `EXECUTION_FAILED`
- `EXECUTION_FAILED` **requires** `action` + `errors` (contains the
action context)
- `INTERNAL_SERVER_ERROR` **requires** `message` (no action context)
## Refactor
- Removed the `relatedFlatEntityMapsKeys` from the `WorkspaceMigration`
type as they're directly inferred from passed actions
- Swallowing actions rollbacks errors in order to iterate over all of
them
## Testing
Created a very straigthforward install application from workspace
migration endpoint in order to start testing the introduced
`WorkspaceMigrationActionExecutionException`
Introduced a feature flag that stop the access to the endpoint if not
enabled
Whole taken direction are totally subjective and highly prone to
mutations ( endpoint location, naming and input schema see
`ts-expect-error` comment )
cc @martmull
## Response error
```ts
{
"eventId": "evt_a1b2c3d4-5678-90ab-cdef-1234567890ab",
"extensions": {
"action": {
"metadataName": "fieldMetadata",
"type": "delete",
"universalIdentifier": "20202020-6110-4547-9fd0-2525257a2c3f"
},
"code": "APPLICATION_INSTALLATION_FAILED",
"errors": {
"metadata": {
"code": "ENTITY_NOT_FOUND",
"message": "Could not find flat entity with universal identifier 20202020-6110-4547-9fd0-2525257a2c3f"
},
"workspaceSchema": {
"code": "ENTITY_NOT_FOUND",
"message": "Could not find flat entity in maps"
}
},
"exceptionEventId": "exc_f9e8d7c6-5432-10ba-fedc-ba0987654321",
"userFriendlyMessage": "Migration execution failed."
},
"message": "Migration action 'delete' for 'fieldMetadata' failed",
"name": "GraphQLError"
}
```
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
<details>
<summary>Dependabot commands and options</summary>
<br />
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
</details>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Abdullah. <125115953+mabdullahabaid@users.noreply.github.com>
Bumps [ts-key-enum](https://gitlab.com/nfriend/ts-key-enum) from 2.0.12
to 2.0.13.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://gitlab.com/nfriend/ts-key-enum/tags">ts-key-enum's
releases</a>.</em></p>
<blockquote>
<h2>v2.0.13</h2>
<p>Remove link to Wikipedia page on teletext.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://gitlab.com/nfriend/ts-key-enum/commit/e9259b2365bbcd82abfb3cfa38a1f1a78041e95a"><code>e9259b2</code></a>
2.0.13</li>
<li><a
href="https://gitlab.com/nfriend/ts-key-enum/commit/5f8a0e079164cad933f1481034d063e088c2a5a3"><code>5f8a0e0</code></a>
MDN update</li>
<li>See full diff in <a
href="https://gitlab.com/nfriend/ts-key-enum/compare/v2.0.12...v2.0.13">compare
view</a></li>
</ul>
</details>
<br />
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
<details>
<summary>Dependabot commands and options</summary>
<br />
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
</details>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Abdullah. <125115953+mabdullahabaid@users.noreply.github.com>
This PR update the redis subscription iterator wrapper with an heartbeat
system. Heartbeat interval are based on event stream TTL. Those refresh
the event stream TTL and active streams in redis.
I also cleaned a few functions that were not used anymore.
---------
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
## Summary
- Adds `exclude-paths` configuration to Dependabot to skip
`packages/twenty-apps/community/**`
- Community-maintained apps have their own dependency management and
don't need the same security requirements as core packages
## Test plan
- Verify Dependabot no longer creates alerts/PRs for dependencies in
community apps
## Summary
Adds `app:build` command as a one-shot version of `app:dev` - builds
manifest, functions, and front components once then exits (no watching).
**Changes:**
- Added `watch` option to `FunctionsWatcher` and
`FrontComponentsWatcher` to support both watch and one-shot modes
- Created `AppBuildCommand` reusing the same build logic as `app:dev`
with `watch: false`
- Added integration tests for `app:build` on both `rich-app` and
`root-app`
- Updated manifest types: `handlerPath` → `sourceHandlerPath` +
`builtHandlerPath`, `componentPath` → `sourceComponentPath` +
`builtComponentPath`
- Updated test app `package.json` scripts to match `create-twenty-app`
template
Implements a new syncable `navigationMenuItem` entity in the core schema
to replace the workspace `favorite` entity.
## Next Steps
- Frontend integration ([separate
PR](https://github.com/twentyhq/twenty/pull/17268))
- Data migration (separate PR)
Summary
- Serverless functions are now built when created/updated instead of at
execution time
- Added builtHandlerPath field to track pre-built function artifacts
- Renamed base-typescript-project to seed-project with pre-built ESM
output included
- Renamed file folder enums for consistency (Functions → BuiltFunction,
SourceCode → Source)
- supports backwards compatibility with existing serverless functions
Tested with all combinations
- storage : local driver and S3 driver
- serverless : local driver and lambda driver
Gmail emails often have multiple labels - an email in "XYZ" label
typically also has "INBOX". The previous negative filter approach
(-label:inbox -label:sent...) excluded these emails entirely, even when
they had a selected label.
Switched to positive OR filtering: (label:cyz OR label:xyz-visible)
-label:spam... which correctly fetches emails with any of the selected
labels regardless of other labels they have.
This edge case wasn't caught earlier because manual testing with INBOX
selected worked fine - it only surfaced when selecting custom labels
without INBOX.
# Introduction
Created a task that allow spawning a term that will run the opened file
and run its integration tests
## Motivation
Jest extension is quite buggy lately, this might just be a tmp
workaround but we can't get `run test` within the test file directly.
Also passing by task allows giving inputs
## Usage
`cmd+shift+P`-> `run task` -> `twenty server - run integration test
file` -> fill inputs prompts:
- watch: will rerun on every related files updates
- updateSnapshot
Note: you can add a custom shortcut to the task too, also it will appear
in task history
Watch mode won't re-create the server instance on each run, meaning that
nest won't hit the `createServer` making very fast to iterate with
Down side of this is that any module injection won't get hydrated
Refactor manifest build and remove src folder assumptions
- Unified entity builder interface: All entity builders now return
EntityBuildResult<T> with both manifests and filePaths
- Always return build result: runManifestBuild now always returns {
manifest, filePaths } instead of null on failure
- Return all entity paths: EntityFilePaths includes paths for all entity
types (application, objects, functions, etc.)
- Remove src folder assumptions: Applications can now have entities at
root level - removed hasSrcFolder checks
- Simplify watchers: Removed entries caching, compute lazily with map()
- Stabilize tests: Replaced flaky console output snapshots with key
message assertions; replaced inline snapshots with array comparisons
-
Fixes https://github.com/twentyhq/core-team-issues/issues/1956
**Problem**
Within an app, the `.yarn/releases/` folder contains executable Yarn
binaries that run when executing any yarn command (`.yarnrc` file
indicates yarn path to be `.yarn/releases/yarn-4.9.2.cjs `.)
This is a supply chain attack vector: a malicious actor could submit a
PR with a compromised `yarn-4.9.2.cjs binary`, which would execute
arbitrary code on developers' machines or CI systems.
**Fix**
Actually, thanks to Corepack, we don't need to store and execute this
binary.
Corepack can be seen as the manager of a package manager: in
`package.json` we indicate a packageManager version like
`"packageManager": "yarn@4.9.2"`, and when executing `yarn` Corepack
will securely fetch the verified version from npm, avoiding the risk of
executing a compromised binary committed to the repository. This was
already in our app's package.json template but we were not using it!
We can now
- remove the folder containing the binary from our app template
base-application (that is scaffolded when creating an app through cli),
`.yarn/releases/`, and remove `yarnPath: .yarn/releases/yarn-4.9.2.cjs`
from its .yarnrc
- remove them from the community apps that were already published in the
repo
- add .yarn to gitignore
**Tested**
This has been tested and works for app created in the repo, outside the
repo, and existing apps in the repo
# Introduction
following https://github.com/twentyhq/twenty/pull/17279
As we've finally identified all the syncable metadata entities, which
means they're expected to have non nullable applicationId and
universalIdentifier at pg_level we can remove previous retro comp
universalIdentifier fallbacking and update the dto too
~~This needs IdentifyRemainingEntitiesMetadataCommand and
MakeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullableMigrationCommand
to be run~~
```ts
[Nest] 197 - 01/21/2026, 3:08:35 PM LOG [MakeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullableMigrationCommand] Successfully run MakeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullableMigrationCommand
```
Fixes#17253
When clicking 'No {Field}' in a polymorphic relation picker, the
relation was not being cleared. This commit implements the missing
detach logic:
- RecordDetailMorphRelationSectionDropdownManyToOne: Call onSubmit with
newValue: null when no item is selected
- MorphRelationManyToOneFieldInput: Call persistMorphManyToOne with
valueToPersist: null for detach case
- useMorphPersistManyToOne: Implement actual detach logic that sets all
morph relation ID fields to null via updateOneRecord
Also adds unit tests for the useMorphPersistManyToOne hook.
# Improve cross-entity duplicate detection in manifest validation
- Refactored findDuplicates to receive the full manifest, enabling
cross-entity duplicate checks
-ObjectExtensionEntityBuilder now validates that extension field IDs
don't conflict with object field IDs
- Renamed DuplicateId type to EntityIdWithLocation for clarity
- Updated all entity builders to use the new signature
Gmail sync was importing messages from folders users had explicitly
disabled because the folder filtering logic lived in the parent service
but the Gmail driver wasn't aware of the import policy when building its
API queries.
Moved messageFolderImportPolicy down to the driver level so Gmail can
- Build proper -label: exclusions during initial sync
- Added `buildGmailLabelSearchName` as our syntax for nested folders was
wrong
- Filter out messages from disabled folders during incremental sync via
history API
This PR handles SSE create event.
It also fixes a regression on board, which was making it flash with
skeletons on any update / create.
This PR was a good test case to experience how each main components of
the app : table, board, calendar, reacts to a create event.
It is not straightforward for now, because each component handles
records with its own state management to turn those records into a
coherent display of rows or cards.
The requests for each component are also different : fetch more, group
by, multiple requests in parallel, so the cleanest way to handle
optimistic effect requires to create one small optimistic engine per
component tuned for its internal data logic.
For now we've decided to implement what's doable in a reasonable amount
of time and that includes not handling table with groups for now.
Creating a clean optimistic logic for each component will be done later.
# QA
## Importing 100 records via the API (could be a script, a workflow, an
AI calling tools, a CSV import, etc.)
https://github.com/user-attachments/assets/d38a3770-8b0a-4f83-8275-5e7d1be0a5c6
## Creating from different components and seeing the SSE event being
processed by other components
https://github.com/user-attachments/assets/106b2f49-19cb-4190-92b7-0653c8373366
# Introduction
As we've been identifying both standard and custom entities for all the
metadata that had standard
We now still need to identify all custom entities enforcing them to have
an `applicationId` and `universalIdentifier`
In this PR we've removed the `SyncableEntityRequired` in favor requiring
props directly in the `SyncableEntity`
Which means that all metadata in db will now expect non nullable
applicationId and universalIdentifier across the whole application
Will add some type cleanup later in
https://github.com/twentyhq/twenty/pull/17277
> [!NOTE]
> This code is temporary. It will be dropped once Record Page Layouts
can be fully configured.
## Before
https://github.com/user-attachments/assets/3f6aed00-2fd5-47d4-bd74-002a68030627
## After
<img width="3456" height="2160" alt="CleanShot 2026-01-20 at 15 17
15@2x"
src="https://github.com/user-attachments/assets/2f9baf0a-e003-4cb6-a023-6e104b097df3"
/>
The `usePageLayoutWithRelationWidgets` hook was appending relation
widgets to the end of the widget list. This resulted in incorrect
ordering where relation widgets appeared after Notes and other widgets
instead of immediately following the FIELDS widget.
## Changes
- **Widget injection logic**: Modified `injectRelationWidgetsIntoLayout`
to find the first FIELDS widget and insert relation widgets immediately
after it, rather than appending to end
- **Notes positioning**: Extract and reposition NOTES widget to appear
after all relation widgets, maintaining correct semantic order: `FIELDS
→ Relations → Notes → Other widgets`
- **Fallback behavior**: When no FIELDS widget exists, append relation
widgets to end as before
- **Test coverage**: Added 6 test cases covering injection order, Notes
positioning, and edge cases (missing widgets, empty relations,
non-record pages)
## Example
Before:
```
[FIELDS, NOTES, GRAPH] → [FIELDS, NOTES, GRAPH, Relation1, Relation2]
```
After:
```
[FIELDS, NOTES, GRAPH] → [FIELDS, Relation1, Relation2, NOTES, GRAPH]
```
> [!WARNING]
>
> <details>
> <summary>Firewall rules blocked me from connecting to one or more
addresses (expand for details)</summary>
>
> #### I tried to connect to the following addresses, but was blocked by
firewall rules:
>
> - `googlechromelabs.github.io`
> - Triggering command: `/usr/local/bin/node /usr/local/bin/node
install.mjs` (dns block)
> -
`https://storage.googleapis.com/chrome-for-testing-public/127.0.6533.88/linux64/chrome-headless-shell-linux64.zip`
> - Triggering command: `/usr/local/bin/node /usr/local/bin/node
install.mjs` (http block)
> -
`https://storage.googleapis.com/chrome-for-testing-public/127.0.6533.88/linux64/chrome-linux64.zip`
> - Triggering command: `/usr/local/bin/node /usr/local/bin/node
install.mjs` (http block)
>
> If you need me to access, download, or install something from one of
these locations, you can either:
>
> - Configure [Actions setup
steps](https://gh.io/copilot/actions-setup-steps) to set up my
environment, which run before the firewall is enabled
> - Add the appropriate URLs or hosts to the custom allowlist in this
repository's [Copilot coding agent
settings](https://github.com/twentyhq/twenty/settings/copilot/coding_agent)
(admins only)
>
> </details>
<!-- START COPILOT ORIGINAL PROMPT -->
<details>
<summary>Original prompt</summary>
Start implementation
<analysis>
[Chronological Review: The conversation began with the user requesting
an update to the `usePageLayoutWithRelationWidgets` function to inject
relation widgets after the first FIELDS widget and to position a Note
widget after all relation widgets. The user then asked for a plan
without comments, followed by a request to start the implementation of
the plan.]
[Intent Mapping:
1. "Update #sym:usePageLayoutWithRelationWidgets. We should inject the
relation widgets directly after the first FIELDS widget."
2. "We want the Note widget to be after all the relation widgets we
inject."
3. "Prepare a plan. Don't write any comment."
4. "Start implementation."]
[Technical Inventory:
- Function: `usePageLayoutWithRelationWidgets`
- Context: This function is part of a page layout management system,
likely dealing with dynamic widget placement based on certain
conditions.]
[Code Archaeology:
- File:
`/Users/devessier/Web/twenty/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutWithRelationWidgets.ts`
- Function Purpose: To manage the layout of widgets on a page,
specifically handling relation widgets and their placement relative to
other widgets.]
[Progress Assessment:
- Completed: User has defined the requirements for widget placement.
- Pending: Implementation of the specified changes to the
`usePageLayoutWithRelationWidgets` function.]
[Context Validation: All necessary context for continuing the
implementation of the widget placement logic is captured, including the
specific order of widget injection.]
[Recent Commands Analysis:
- Last Agent Commands: The user requested to "Start implementation."
- Tool Results Summary: No specific tool results were generated as the
conversation focused on planning and implementation requests.
- Pre-Summary State: The agent was actively preparing to implement the
changes to the `usePageLayoutWithRelationWidgets` function when the
token budget was exceeded.
- Operation Context: The commands executed were directly related to the
user's goal of modifying the widget layout in the application.]
</analysis>
<summary>
1. Conversation Overview:
- Primary Objectives:
- "Update #sym:usePageLayoutWithRelationWidgets. We should inject the
relation widgets directly after the first FIELDS widget."
- "We want the Note widget to be after all the relation widgets we
inject."
- "Prepare a plan. Don't write any comment."
- "Start implementation."
- Session Context: The user is focused on modifying the widget layout in
a page layout management system, specifically the order of relation and
Note widgets.
- User Intent Evolution: The user transitioned from defining
requirements to requesting a plan and then moving directly to
implementation.
2. Technical Foundation:
- Function: `usePageLayoutWithRelationWidgets`
- Purpose: To manage the layout of widgets on a page, particularly the
placement of relation widgets.
3. Codebase Status:
- File Name: `usePageLayoutWithRelationWidgets.ts`
- Purpose: To handle the layout of widgets dynamically based on
specified rules.
- Current State: Awaiting implementation of the user-defined widget
placement logic.
- Key Code Segments: The function signature is defined, but the
implementation details are yet to be added.
4. Problem Resolution:
- Issues Encountered: No specific technical problems were reported; the
focus was on planning and implementation.
- Solutions Implemented: None yet, as the implementation phase has just
begun.
- Debugging Context: No ongoing troubleshooting efforts were mentioned.
- Lessons Learned: The importance of clear widget placement requirements
was emphasized.
5. Progress Tracking:
- Completed Tasks: User has articulated the requirements for widget
placement.
- Partially Complete Work: Implementation of the specified changes is
pending.
- Validated Outcomes: No features have been confirmed working yet as
implementation has not started.
6. Active Work State:
- Current Focus: The user is preparing to implement the changes to the
`usePageLayoutWithRelationWidgets` function.
- Recent Context: The user has defined the order of widget placement and
is ready to start coding.
- Working Code: The function is currently defined but lacks the
implementation logic.
- Immediate Context: The user is focused on implementing the logic for
injecting relation widgets and positioning the Note widget.
7. Recent Operations:
- Last Agent Commands: "Start implementation."
- Tool Results Summary: No specific results were generated; the focus
was on user requests.
- Pre-Summary State: The agent was preparing to implement the changes to
the `usePageLayoutWithRelationWidgets` function.
- Operation Context: The commands executed were directly related to the
user's goal of modifying the widget layout.
8. Continuation Plan:
- Pending Task 1: Implement the logic to inject relation widgets after
the first FIELDS widget.
- Pending Task 2: Ensure the Note widget is positioned after all
relation widgets...
</details>
<!-- START COPILOT CODING AGENT SUFFIX -->
Created from [VS
Code](https://code.visualstudio.com/docs/copilot/copilot-coding-agent).
<!-- START COPILOT CODING AGENT TIPS -->
---
💬 We'd love your input! Share your thoughts on Copilot coding agent in
our [2 minute survey](https://gh.io/copilot-coding-agent-survey).
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Devessier <29370468+Devessier@users.noreply.github.com>
Co-authored-by: Devessier <baptiste@devessier.fr>
## Overview
This PR implements **generic many-to-many relation support** through
junction tables (also known as associative entities or join tables).
This replaces the need for hardcoded taskTarget/noteTarget logic and
provides a flexible foundation for modeling complex entity
relationships.
## Architecture
### Data Model
Many-to-many relationships are implemented using a **junction object
pattern**:
```
┌─────────┐ ┌──────────────────┐ ┌─────────┐
│ Pet │──────>│ PetRocket │<──────│ Rocket │
│ │ 1:N │ (junction) │ N:1 │ │
│ rockets ├───────┤ pet : Pet ├───────┤ │
└─────────┘ │ rocket : Rocket │ └─────────┘
└──────────────────┘
```
The junction object (PetRocket) has:
- A `MANY_TO_ONE` relation to **Pet** (the source)
- A `MANY_TO_ONE` relation to **Rocket** (the target)
The source object (Pet) has a `ONE_TO_MANY` relation pointing to the
junction, with **field settings** that specify which target field to
follow.
### Field Settings Schema
Junction configuration is stored in `FieldMetadataRelationSettings`:
```typescript
{
relationType: "ONE_TO_MANY",
// Points to the target field on the junction object
junctionTargetFieldId?: string; // For regular relations
junctionTargetMorphId?: string; // For polymorphic relations
}
```
**Two configuration modes:**
1. **`junctionTargetFieldId`** - References a specific `RELATION` field
on the junction
2. **`junctionTargetMorphId`** - References a `morphId` group for
polymorphic targets (e.g., link to Person OR Company)
### GraphQL Query Generation
When a junction relation is detected, the GraphQL fields are generated
to fetch the nested target:
```graphql
query GetPetWithRockets {
pet(id: "...") {
rockets { # ONE_TO_MANY to junction
id
rocket { # Target field on junction
id
name
__typename
}
}
}
}
```
For polymorphic junction targets:
```graphql
caretakerPerson { id, name }
caretakerCompany { id, name }
```
## Frontend Architecture
### Display Flow
1. **Detection**: `hasJunctionConfig()` checks if field has junction
settings
2. **Config Resolution**: `getJunctionConfig()` resolves junction object
metadata and target fields
3. **Record Extraction**: `extractTargetRecordsFromJunction()` extracts
target records from junction records
4. **Rendering**: Target records displayed as chips (not junction
records)
### Edit Flow
1. **Picker Opening**: Initializes the multi-record picker with:
- Searchable object types (derived from junction target fields)
- Pre-selected items (extracted from existing junction records)
2. **Selection Handling**: Manages create/delete of junction records:
- **Select**: Creates new junction record with source + target IDs
- **Deselect**: Finds and deletes the junction record
- **Optimistic Updates**: Manually updates Recoil store before API call
### Key Trade-offs
| Decision | Trade-off |
|----------|-----------|
| Junction records managed manually | More control over optimistic
updates, but requires manual cache management |
| Settings stored per-field | Flexible (same junction can power
different views), but requires UI to configure |
| Polymorphic via morphId groups | Supports N target types, but adds
query complexity |
| Feature flag gated | Safe rollout, but requires flag management |
## Backend Changes
- **Validation**: Junction target field must exist and be a valid
`MANY_TO_ONE` relation
- **Settings**: Extended `FieldMetadataRelationSettings` type with
junction fields
- **Dev Seeder**: Added sample junction objects (PetRocket,
EmploymentHistory, PetCareAgreement) for testing
## How to Test
1. Enable the `IS_JUNCTION_RELATIONS_ENABLED` feature flag
2. Create objects with junction pattern (Pet → PetRocket → Rocket)
3. Configure the junction target in field settings (advanced mode)
4. Verify:
- Display shows target objects (Rockets), not junction records
(PetRockets)
- Picker allows selecting/deselecting targets
- Changes persist correctly
https://github.com/user-attachments/assets/d04f057a-228c-4de8-af48-76bb2d72cac1
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
# Introduction
In this PR we're migrating the serverless function service that was
using the SF repo directly to the v2 build and runner.
The whole serverless engine now deals with flat entities only
## Resolvers
Refactored the resolvers ( serverlessFunction, route, database and cron
trigger) :
- return types to `dto`
- Standardized the flat to dto transpilation within the resolvers
- Find and findMany passing by the cached data
## Services
Refactored the services ( serverlessFunction, route, database and cron
trigger) :
- return type to be `flat`
- always calling v2 and computing cache
## New additional caches
- application variables ( cf
https://github.com/twentyhq/core-team-issues/issues/2116 )
- serverless function layer
## What to test:
- CRUD ( database trigger ✅ , route trigger, cron trigger, serverless
function through workflows ✅ )
- Duplicating a workflow with a serverless function code node ✅
## Concerns
We need to implement the cron that will hard delete soft deleted s3
serverless functions, not in this PR though ( cf
https://github.com/twentyhq/twenty/pull/17285#discussion_r2709168570 and
https://github.com/twentyhq/core-team-issues/issues/2118 )
In this PR :
- New field type FieldMetadataType.FILES added (as jsonb in DB)
- Backend: GraphQL types, validation, REST API schema, data processor
handling
- Frontend: field configuration in settings
- Feature flag IS_FILES_FIELD_ENABLED to gate the feature
- Integration tests for create/filter validation
- Dev seed: Feature flag enabled + FILES field on survey result object
To do in next PRs :
- Backend :
-- new workspaceFile controller (for download) & new workspaceFile
upload resolver (for upload)
-- pre-hook/post-hook to ensure fileId existence + listener to ensure
cleaning
- Frontend : FILES field display/edit in table view, record, ...
Workflows can now be cancelled when not started or enqueued.
Also displaying the stop option when selecting all.
We got the case of a client that did an infinite loop. So he had a lot
of workflows to stop. Supporting select all would have avoid him to wait
for 4 hours so we process the runs throttled. This is not something that
is supposed to happen often so I did not see the point of refactoring
the endpoint. But I wanted the users to have this option just in case
Heavy Refactoring of the watcher, sorry about this one, I'll keep
iterating on it.
In a nutshell:
- app-dev.ts is maintaining 3 watchers in parallel: manifest, function
and frontComponent
## Context
- Add RLS entitlement to billing
- Check value in the backend (for RLS predicate entity
queries/mutations)
- Expose billingEntitlements to the API inside currentWorkspace to check
available features to the workspace and display the role components
accordingly
- Cleanup RLS when plan changes back to one without RLS.
This should cover almost everything, imho we don't need to check in the
ORM because => We can't create RLS without the correct PLAN and
switching back to a PLAN without RLS deletes existing RLS through
stripes webhooks
Fetch and send full workspace member to handle dynamic predicate. I only
tested that it did not break the current logic since the frontend does
not yet send queries we dynamic predicates.
- Fix a race condition due to a page change effect re-trigger which
reset the focus stack (A user had a bug which happened when he opened
the command menu quickly after the page load. The focus stack was reset
and the hotkeys listening were the one from the table instead of the one
from the command menu)
- Fix the focus id in the side panel input which prevented using the
hotkeys
closes https://github.com/twentyhq/core-team-issues/issues/2097
- Align cumulative range filtering with non‑cumulative behavior by
applying rangeMin/rangeMax to raw values before cumulative totals are
computed.
- Remove cumulative‑value filtering to prevent hidden points from
influencing visible totals.
## Add `frontComponent` entity type to SDK
This PR adds a new `frontComponent` entity type at parity with
`serverlessFunction`. Front components are React components that can be
defined and bundled as part of Twenty applications.
### Changes
#### New Entity Type
- Added `FRONT_COMPONENT` to `SyncableEntity` enum
- Front components use `*.front-component.tsx` file naming convention
- CLI command `twenty app:add` now includes `front-component` as an
option
#### SDK Application Layer (`twenty-sdk`)
- Added `defineFrontComponent()` function for defining front component
configurations with validation
- Added `FrontComponentConfig` type for component configuration
- Added `getFrontComponentBaseFile()` template generator for scaffolding
new front components
- Added `loadFrontComponentModule()` to load front component modules and
extract component metadata
#### Shared Types (`twenty-shared`)
- Added `FrontComponentManifest` type with `universalIdentifier`,
`name`, `description`, `componentPath`, and `componentName` fields
- Updated `ApplicationManifest` to include optional `frontComponents`
array
#### Manifest Build System
- Updated `manifest-build.ts` to discover and load
`*.front-component.tsx` files
- Updated `manifest-validate.ts` to validate front components and check
for duplicate IDs
- Updated `manifest-display.ts` to display front component count in
build summary
- Updated `manifest-plugin.ts` to display front component entry points
and watch `.tsx` files
#### Template (`create-twenty-app`)
- New applications now include a sample
`hello-world.front-component.tsx` file
#### Tests
- Added unit tests for `defineFrontComponent()`
- Added unit tests for `getFrontComponentBaseFile()`
## Context
buildValueFromFilter was not handling composite filters which was needed
for RLS. This PR implements that and fix some issues with RLS
Tested with a few composite + relation fields
<img width="559" height="206" alt="Screenshot 2026-01-19 at 15 38 42"
src="https://github.com/user-attachments/assets/d64afa6e-3e12-4843-a215-a693665112c5"
/>
## Summary
This PR adds function build support to the `twenty dev` command,
enabling tree-shaken production builds of serverless functions during
development with Vite's incremental build capabilities.
## Changes
### New Features
- **Function building in dev mode**: Serverless functions are now
compiled to tree-shaken bundles in `.twenty/functions/` during
development
- **Automatic restart on entry point changes**: When functions are added
or removed, the watcher automatically restarts with the new
configuration
- **Function entry point logging**: Dev mode now displays detected
function entry points with their names and paths
- Create resolvers for each type of charts which needs data
transformation after the group by operation: Bar Chart, Line Chart and
Pie Chart
- Move all the utils to the backend and refactored some into services
This allows all the computation to be done in the backend, improving
performances in the frontend.
## Context
Due to RLS feature, workspaceMember entity and its properties is
becoming necessary in different places of the backend. To avoid querying
many times, this PR adds a map of workspace members per workspaceId,
leveraging WorkspaceCache pattern for WorkspaceEntity (in opposition
with CoreEntity)
Due to its content (mostly PII), I prefer to cache it locally only (node
process VS Redis) using localDataOnly.
TODO: invalidation logic when the workspaceMember object is updated
(more precisely, when its field map is updated). There is no easy way
today to update that list so it can be done in another PR imho
https://github.com/user-attachments/assets/3a444937-0290-4505-85e1-64b7b713bc3b
- [x] Analyze the existing `FieldWidgetRelationCard` and
`FieldWidgetMorphRelationCard` components
- [x] Review existing patterns for "More" buttons (found
`IconChevronDown` pattern)
- [x] Create a reusable `FieldWidgetShowMoreButton` component for the
"More (X)" button
- [x] Update `FieldWidgetRelationCard` to show max 5 items initially
with progressive loading
- [x] Update `FieldWidgetMorphRelationCard` to show max 5 items
initially with progressive loading
- [x] Add Storybook story with play function to test progressive loading
for regular relations
- [x] Verify changes visually in Storybook (all tests pass)
- [x] Run code review (no issues found)
- [x] Run CodeQL security check (no code changes for CodeQL to analyze)
- [x] Address PR review feedback:
- [x] Move constants to separate files
(`FieldWidgetRelationCardInitialVisibleItems.ts` and
`FieldWidgetRelationCardLoadMoreIncrement.ts`)
- [x] Add translation for "More (X)" string using `t` macro from
`@lingui/core/macro`
## Summary
This PR adds progressive loading functionality to the FIELD widget with
CARD display mode:
1. **New Component**: Created `FieldWidgetShowMoreButton` - A styled
button component that displays "More (X)" with a chevron icon, showing
the remaining count of hidden items.
2. **Updated Components**:
- `FieldWidgetRelationCard`: Now displays max 5 relation cards
initially, with a "More (X)" button that loads 5 more items on each
click
- `FieldWidgetMorphRelationCard`: Same progressive loading behavior for
morph relations
3. **Constants**: Moved to separate files following codebase
conventions:
- `FIELD_WIDGET_RELATION_CARD_INITIAL_VISIBLE_ITEMS` (5)
- `FIELD_WIDGET_RELATION_CARD_LOAD_MORE_INCREMENT` (5)
4. **Storybook Story**: Added
`OneToManyRelationCardWidgetWithProgressiveLoading` story with play
function that tests:
- Initial display of 5 items
- "More (7)" button visibility
- Loading additional items on click
- Button disappearing when all items are shown
<!-- START COPILOT ORIGINAL PROMPT -->
<details>
<summary>Original prompt</summary>
>
> ----
>
> *This section details on the original issue you should resolve*
>
> <issue_title>[RPL] Allow users to progressively load relations in
FIELD widget with CARD display mode</issue_title>
> <issue_description>## Current state
>
> The FIELD widget should display max 5 relations in CARD display mode.
>
> ## Goal
>
> Display a "More (12)" button. Clicking on this button loads 5 more
items. After a click, "More (12)" becomes "More (7)".
>
> <img width="1334" height="712" alt="Image"
src="https://github.com/user-attachments/assets/e801cca2-af99-4d87-8ce2-0c639775b0b1"
/>
>
> ## Technical input
>
> `FieldWidgetRelationCard` already loads all the relations and maps
over each one of them. Create a state that's updated when clicking on
the More button, slicing the relations to display.
>
> ## Acceptance criteria
>
> - Must work for relations and morph relations in _card_ display mode
> - Create stories to assert it works properly (with play function to
actually test the components)</issue_description>
>
> ## Comments on the Issue (you are @copilot in this section)
>
> <comments>
> </comments>
>
</details>
<!-- START COPILOT CODING AGENT SUFFIX -->
- Fixestwentyhq/core-team-issues#2063
<!-- START COPILOT CODING AGENT TIPS -->
---
💡 You can make Copilot smarter by setting up custom instructions,
customizing its development environment and configuring Model Context
Protocol (MCP) servers. Learn more [Copilot coding agent
tips](https://gh.io/copilot-coding-agent-tips) in the docs.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Devessier <29370468+Devessier@users.noreply.github.com>
Co-authored-by: Baptiste Devessier <baptiste@devessier.fr>
Summary
Rewrites the app:dev command to use Vite's dev server instead of manual
file watching with chokidar. This provides better file watching
capabilities and aligns with the existing build tooling.
<img width="1588" height="822" alt="image"
src="https://github.com/user-attachments/assets/7c54bd39-4905-456f-8e3d-64e97b031de0"
/>
# Introduction
Related to https://github.com/twentyhq/core-team-issues/issues/1989
1/ Migration, applicationId and universalIdentifier are required on
entity ( save point migration + upgrade command fallback pattern )
2/ Backfill using previous standard ids
## Test
tested prod extract
Closes https://github.com/twentyhq/core-team-issues/issues/1838
DatabaseEventTriggers can now define a field-level granularity on which
updates they should react to.
After a discussion with the team, we have decided to rely on field names
and not UID, just as we do for objects. The rationale is that we want to
keep a pleasant devX and consider it's not on twenty to ensure
continuity of api usage around an object if their name is updated: for
instance if a user has based their webhook on the name of an object, if
they decide to update it, they have to take care of updating their
webhook.
# Introduction
Related to https://github.com/twentyhq/core-team-issues/issues/1989
1/ Migration, applicationId and universalIdentifier are required on
entity ( save point migration + upgrade command fallback pattern )
2/ Backfill using previous standard ids
## Test
tested prod extract
## Note
Added build to typeorm nx generate migration command so we never forgot
to build server before
## Description
This PR improves the developer experience for the `twenty-sdk` and
`create-twenty-app` packages by reorganizing commands, adding
development tooling, and improving documentation.
## Changes
### twenty-sdk
#### Command Refactoring
- **Renamed `app-watch` → `app-dev`**: Renamed `AppWatchCommand` to
`AppDevCommand` and moved to `app-dev.ts` for consistent naming with the
CLI command `app:dev`
- **Moved `app-add` → `entity-add`**: Relocated entity creation logic
from `app/app-add.ts` to `entity/entity-add.ts` and renamed
`AppAddCommand` to `EntityAddCommand` for better separation of concerns
#### Development Tooling
- **Added `dev` target**: New Nx target `npx nx run twenty-sdk:dev` that
runs the build in watch mode for faster development iteration
### create-twenty-app
#### Improved Scaffolded Project
- **Enhanced base-application README** with:
- Updated Getting Started to recommend `yarn dev` for development
- Added "Available Commands" section listing all available scripts
#### Better Onboarding
- **Improved success message** after app creation with formatted next
steps:
Flow:
- fetch user role from context stored in cache - do not support api
context yet
- check object permissions
- filter restricted fields on event
- fetch role rls predicate and combine it with the query filter
Do not support dynamic predicates yet.
# Introduction
Related to https://github.com/twentyhq/core-team-issues/issues/1989
1/ Migration, applicationId and universalIdentifier are required on
entity ( save point migration + upgrade command fallback pattern )
2/ Backfill using previous standard ids
## Test
tested prod extract
## Summary
- Fix E2E login test flakiness by using `click()` auto-waiting instead
of `isVisible()` check
- The login form shows a loader while GraphQL data loads. The previous
`isVisible()` check returned immediately (no waiting) and would fail
while the loader was showing
- Using `click()` which has built-in auto-waiting for elements to be
visible and actionable fixes this
## Test plan
- E2E tests should pass more reliably in CI
- Login setup test should no longer timeout waiting for the email field
## Summary
Fixes#17101
When self-hosting TwentyCRM, users can now set workspace custom domains
without requiring CLOUDFLARE_API_KEY to be configured. This enables
manual DNS configuration for those not using Cloudflare.
## Changes
- Added isCloudflareConfigured method to DnsManagerService
- Modified all Cloudflare-dependent methods to gracefully handle missing
configuration
- Added getManualDnsRecords helper that provides DNS configuration
instructions when Cloudflare is not available
- isHostnameWorking returns true in manual mode, allowing the domain to
be saved and enabled
- Added comprehensive tests for non-Cloudflare scenarios
## Behavior
When CLOUDFLARE_API_KEY is set: Works exactly as before with automatic
Cloudflare provisioning
When CLOUDFLARE_API_KEY is NOT set:
- Custom domain can be saved to the database
- User receives manual DNS configuration instructions
- Domain is marked as working, user is responsible for external DNS and
TLS configuration
## Testing
Added tests covering non-Cloudflare scenarios. Full test suite requires
Docker which was not run locally.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> Introduces a Cloudflare integration feature flag and wires it through
server and front to gate the custom domain UI.
>
> - Server: adds `isCloudflareIntegrationEnabled` to `ClientConfig`,
computed from `CLOUDFLARE_API_KEY` and `CLOUDFLARE_ZONE_ID` in
`client-config.service`; updates GraphQL schema and unit tests.
> - Frontend: adds `isCloudflareIntegrationEnabledState`, extends
`ClientConfig` type, sets the flag in `useClientConfig`, and
conditionally renders `SettingsCustomDomain` in `SettingsDomain` when
enabled.
> - Updates mocked client config to include the new flag.
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
0c76dde03b. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Signed-off-by: Vedant Madane <6527493+VedantMadane@users.noreply.github.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
FIELD widgets displaying ONE_TO_MANY relations now show a "See all"
action button (arrow-up-right icon) that navigates to the record index
with filtered relations.
### Demo
https://github.com/user-attachments/assets/fd632553-70e3-4757-8f26-80aa8d2ce0d2
### Changes
- **`WidgetAction` type**: Added `'see-all'` to `WidgetActionId`
- **`useWidgetActions` hook**: Returns `'see-all'` action for
ONE_TO_MANY relation fields (position 0, before edit)
- **`WidgetActionFieldSeeAll` component**: Renders the navigation button
with:
- `IconArrowUpRight` icon
- Link computed using same logic as `RecordDetailRelationSection`
- Hover visibility behavior matching existing edit button
- **`WidgetActionRenderer`**: Added case for `'see-all'` action
- **Storybook**: Added `OneToManyRelationFieldWidgetWithSeeAllButton`
story verifying button visibility and well-formed link
### Link computation
Uses the same filter URL pattern as `RecordDetailRelationSection`:
```typescript
const filterQueryParams = {
filter: {
[relationFieldMetadataItem.name]: {
[ViewFilterOperand.IS]: {
selectedRecordIds: [targetRecord.id],
},
},
},
viewId: indexViewId,
};
const filterLinkHref = getAppPath(
AppPath.RecordIndexPage,
{ objectNamePlural: relationObjectMetadataItem.namePlural },
filterQueryParams,
);
```
The "see-all" action is shown regardless of field read-only status since
it's a navigation action, not an edit action.
<!-- START COPILOT ORIGINAL PROMPT -->
<details>
<summary>Original prompt</summary>
>
> ----
>
> *This section details on the original issue you should resolve*
>
> <issue_title>[RPL] Display "See all" widget action for FIELD widget
displaying relations</issue_title>
> <issue_description>The "See all" widget action is the button you can
see in the top right corner in the following screen. We should redirect
the user to the record index showing the filtered relations.
>
> <img width="1334" height="712" alt="Image"
src="https://github.com/user-attachments/assets/c8aca25e-2136-43b3-8896-abd161a5693e"
/>
>
> Example of interaction today in production:
>
>
https://github.com/user-attachments/assets/3730301c-d04b-4195-b2fb-a51f8efee08a
>
> ## Technical details
>
> See `useWidgetActions` and `WidgetActionRenderer`. Use the
`arrow-up-right` icon.
>
> See how link is computed here:
https://github.com/twentyhq/twenty/blob/3cf7803ee1ae545add02f0c628a933618ce736d5/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSection.tsx#L193-L204.
Reuse the same link.
>
> ## Acceptance criteria
>
> - The action should be displayed in addition to other actions that
might be rendered today
> - The action must only be displayed for ONE_TO_MANY relations
> - Ensure you write at least one story to ensure the button is
correctly displayed; the button should a well formed
link</issue_description>
>
> ## Comments on the Issue (you are @copilot in this section)
>
> <comments>
> </comments>
>
</details>
<!-- START COPILOT CODING AGENT SUFFIX -->
- Fixestwentyhq/core-team-issues#2064
<!-- START COPILOT CODING AGENT TIPS -->
---
💡 You can make Copilot smarter by setting up custom instructions,
customizing its development environment and configuring Model Context
Protocol (MCP) servers. Learn more [Copilot coding agent
tips](https://gh.io/copilot-coding-agent-tips) in the docs.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Devessier <29370468+Devessier@users.noreply.github.com>
Co-authored-by: Devessier <baptiste@devessier.fr>
- Add URL validation in getImageBufferFromUrl utility
- Add response status validation and content-type checking
- Add timeout and connection error handling with specific error messages
- Validate buffer is not empty before processing
- Validate file type detection results before proceeding
- Ensure detected file type is actually an image format
- Add proper type safety for Axios error handling
This improves robustness when uploading images from URLs by:
- Preventing invalid URLs from being processed
- Providing clear error messages for different failure scenarios
- Ensuring only valid image files are processed
- Handling network errors gracefully
---------
Co-authored-by: GitTensor Miner <miner@gittensor.io>
Opening a PR to merge the wip work by @martmull
We will re-organize twenty-sdk in an upcoming PR
---------
Co-authored-by: martmull <martmull@hotmail.fr>
In this PR
- handle settings permission check for applications. Until then this was
unhandled and applications could not perform actions requiring settings
permissions, even if they were granted them
- fix ties to attachment, noteTarget etc:
When an object had their fields synchronized in the app, the system
fields created as a side-effect of the object creation (relations to
noteTarget, attachment, taskTarget, favorites, timelineActivities -
created with `isCustom: true`, not sure that is correct btw) were then
deleted because they are not declared in the app, and identified as
deletable because of `isCustom: true`.
Updating the logic to exclude system fields from the logic that detects
fields to delete.
I think this outline the confusion we have around isCustom, isSystem
etc.
- introduce uploadFile util in generated twenty client as it cannot be
handled by the client's query / mutation. I had to use this for my
invoicing app
# Introduction
Related to https://github.com/twentyhq/core-team-issues/issues/1989
1/ Migration, applicationId and universalIdentifier are required on
entity ( save point migration + upgrade command fallback pattern )
2/ Backfill using previous standard ids
## Test
tested prod extract
This PR implements SSE update events across the main components of the
application : tables, boards, calendars, show pages.
There is still work to do on other event type and on making sure
everything works fine, but this first implementation should be robust
enough to start with.
Some problems encountered along the way :
- Events are returning raw Postgres output, because they are not
normalized by the GraphQL layer, so we ended up with amountMicros as
string values, which the frontend does not like, so I implemented a
small util in our `formatResult` generic pipeline to turn amountMicros
to a number if it's a string value. We could implement other formatters
for composite fields if we see problems with events.
-`action` property was missing in SSE events, which is required by the
frontend to know what kind of event it is.
# QA
https://github.com/user-attachments/assets/393b45ac-59d2-48b0-855b-2ce9c4b8ae57https://github.com/user-attachments/assets/cb214e7a-1595-4b85-bdb6-87630993d2e2
# Introduction
Related to https://github.com/twentyhq/core-team-issues/issues/1989
1/ Migration, applicationId and universalIdentifier are required on
entity ( save point migration + upgrade command fallback pattern )
2/ Backfill using previous standard ids
## Test
tested prod extract
## Summary
Moves the custom ESLint rules from `tools/eslint-rules` to
`packages/twenty-eslint-rules` for better organization within the
monorepo packages structure.
## Changes
- Move `eslint-rules` from `tools/` to `packages/twenty-eslint-rules`
- Use `loadWorkspaceRules` from `@nx/eslint-plugin` to load custom rules
- Update all ESLint configs to use the `twenty/` rule prefix instead of
`@nx/workspace-`
- Update `project.json`, `jest.config.mjs` with new paths
- Update `package.json` workspaces and `nx.json` cache inputs
- Update Dockerfile reference
## Technical Details
The custom ESLint rules are now loaded using Nx's `loadWorkspaceRules`
utility which:
- Handles TypeScript transpilation automatically
- Allows loading workspace rules from any directory
- Provides a cleaner approach than the previous `@nx/workspace-`
convention
## Testing
- Verified all 17 custom ESLint rules load correctly from the new
location
- Verified linting works on dependent packages (twenty-front,
twenty-server, etc.)
As part of the extensibility effort, we are introducing a new engine
entity called "Front Component". This represents a dynamic react
component that will be rendered in CommandMenu actions or in PageLayout
widgets
This PR introduce the entity and all the necessary boilerplate to make
it syncable and cachable in the engine
## Problem
The Crowdin GitHub Actions were failing with:
```
❌ No sources found for 'packages/twenty-docs/user-guide/**/*.mdx' pattern
❌ No sources found for 'packages/twenty-docs/developers/**/*.mdx' pattern
❌ No sources found for 'packages/twenty-docs/twenty-ui/**/*.mdx' pattern
❌ No sources found for 'packages/twenty-docs/navigation/navigation.template.json' pattern
```
## Root Cause
The Crowdin config files are located at `.github/crowdin-docs.yml` and
`.github/crowdin-app.yml`. By default, the Crowdin CLI resolves source
paths relative to the config file's directory (`.github/`), not the
repository root.
So paths like `packages/twenty-docs/...` were being resolved as
`.github/packages/twenty-docs/...`, which doesn't exist.
## Fix
Added `base_path: ".."` to both Crowdin config files to make paths
resolve relative to the repository root.
Got rid of filters for now -- filters would need extra tailoring since
they depend on field names/types and the RecordGqlOperationFilter DSL
(nested AND/OR and composite subfields), which the AI can’t reliably
construct without additional metadata and skills
created a followup to tackle the same
https://github.com/twentyhq/core-team-issues/issues/2108
# Introduction
~~Testing first this might break some constraints can't remember the
initial motivation~~ -> it does not
The goal here is to avoid relying on pg cascading which will lock the
schema longer than if done in n operations
- add `forwardedRequestHeaders` `string[]` column in `core.routeTrigger`
- filter request headers and forward filtered headers to function
payload (avoid spreading unexpectedly token or cookie)
- add `forwardedRequestHeaders` option in twenty-sdk `defineFunction`
util
BREAKING for actual routeTrigger payload but only 16 to migrate in
production
## Context
Now that RLS predicates are applied, creating a record through the FE
(which is empty by default) is failing if your role has predicates and
your input does not respect them (which will always be true since, as
said above, input will be pretty much empty)
## Implementation
- Moved isMatching* filters to twenty-shared
- Implemented isMatchingRlsPredicates utils in the backend (ORM) to
check before insertion/update if the record is matching the current user
role Rls predicates, reusing the isMatching* filters utils moved to
twenty-shared
- Frontend now applies RLS predicates before creating a new record
(similarly to what we do with view filters)
Note:
It seems composite were not properly handled with view-filter insertion
logic, since I'm reusing the util for now, the issue remains for RLS and
will need to be addressed
This PR refactors object record operation dispatch through a browser
event instead of a state that was registering all operations.
This is a cleaner pattern as it is a synchronous event code path instead
of relying on a useEffect to watch state change, which is not ideal.
We introduce this change first to then rely on this new pattern to
dispatch SSE events in a following PR.
## Summary
Fixes a regression where the save button was not visible when creating a
new role.
## Root Cause
PR #17062 (RLS FE implementation) introduced a change to the `isDirty`
logic that added `isDefined(settingsPersistedRole)` as a condition:
```typescript
const isDirty =
isDefined(settingsPersistedRole) &&
!isDeeplyEqual(settingsDraftRole, settingsPersistedRole);
```
However, in create mode, `settingsPersistedRole` is intentionally set to
`undefined` (in `SettingsRoleCreateEffect.tsx`), causing `isDirty` to
always evaluate to `false` and hiding the save button.
## Fix
Added `isCreateMode` to the `isDirty` condition so the save button shows
when creating a new role:
```typescript
const isDirty =
isCreateMode ||
(isDefined(settingsPersistedRole) &&
!isDeeplyEqual(settingsDraftRole, settingsPersistedRole));
```
cc @Weiko
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> Restores save button visibility when creating a role.
>
> - Simplifies `isDirty` to `!isDeeplyEqual(settingsDraftRole,
settingsPersistedRole)`, removing the `isDefined(settingsPersistedRole)`
check so create-mode is considered dirty.
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
21593d54c3. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Weiko <corentin@twenty.com>
# Introduction
Instead of comparing existing to expected comparing expected to existing
Also improved logs and fallback use cases
# Test
Tested on a prod extract
Fixes https://github.com/twentyhq/twenty/issues/16775
When a field is deleted from the model, the workflow action step still
stores the deleted field name in `step.settings.input.objectRecord`.
We need to filter out the fields that are not valid anymore. This logic
existed before but had been removed with the migration to tool services.
# Introduction
In the https://github.com/twentyhq/core-team-issues/issues/1989 's
context we will apply migration through an upgrade command post entity
backfill to fit the applied constraint. But suspended soft deleted
workspace are not included in the upgrade workspace batches
In order to be able to pass the constraint in production, discussed with
@FelixMalfait, we will manually clear all the currently suspended and
soft deleted workspace ignore their grace period
## Force mode
When running the command in force it will ignore the limit per execution
( which really serve the cron job ) and the grace period
## Test
Tested on a production extract
# Introduction
Related https://github.com/twentyhq/core-team-issues/issues/1989
1/ Migration, applicationId and universalIdentifier are required on
entity ( save point migration + upgrade command fallback pattern )
2/ Backfill using previous `standardId` or `isCustom`
## Test
Both tested on prod extract
Some view field are set as non custom is prod whereas they should for
several manually handle-able workspace amount
This PR fixes two critical bugs with the DATE type, following up the
recent refactor around dates and time zones :
https://github.com/twentyhq/twenty/pull/16544
There are a lot of related bugs in Sentry, this PR should fix all bugs
that are of the form :
`Cannot parse: 2026-02-04T00:00:00.000Z`
# `node-postgres` date type
The package `node-postgres`, used by TypeORM, returns by default a Date
object for the date OID type.
But we can change this behavior without patching anything.
See the documentation for `pg-types` :
https://github.com/brianc/node-pg-types
Our fix is to call `setTypeParser` from `pg` to have the postgres "date"
type returned as a string always.
```ts
export const setPgDateTypeParser = () => {
types.setTypeParser(PG_DATE_TYPE_OID, (val: string) => val);
};
```
Then in server's main.ts :
```ts
const bootstrap = async () => {
setPgDateTypeParser();
```
## Are we safe with this very low-level fix ?
Since TypeORM bypasses a string value returned by `pg` we are safe with
this modification at a very low-level.
```ts
else if (columnMetadata.type === "date") {
value = DateUtils.mixedDateToDateString(value)
}
```
https://github.com/typeorm/typeorm/blob/0ec4079cd3760dc49247a02c54415f16a2a51766/src/driver/postgres/PostgresDriver.ts#L838
```ts
/**
* Converts given value into date string in a "YYYY-MM-DD" format.
*/
static mixedDateToDateString(value: string | Date): string {
if (value instanceof Date) {
return (
this.formatZerolessValue(value.getFullYear(), 4) +
"-" +
this.formatZerolessValue(value.getMonth() + 1) +
"-" +
this.formatZerolessValue(value.getDate())
)
}
return value
}
```
https://github.com/typeorm/typeorm/blob/6f3788b83730463e3b787b2a98bb41695e13caf8/src/util/DateUtils.ts#L28
For reference the original type parser seems to be configured in
node-pg-types, on the 1182 OID, which is an array of date / 1082 OID,
this type parser calls another small library `postgres-date` which
creates a JS Date. I couldn't find a type parser explicitly on 1082 in
the stack TypeORM -> node-postgres -> node-pg-types -> postgres-date
```ts
register(1182, parseStringArray) // date[]
```
https://github.com/brianc/node-pg-types/blob/26bfe645a8ddc0c73830b1b8c63f2c4f8265b24f/lib/textParsers.js#L168C19-L168C45
```ts
static parse (dateString) {
return new PGDateParser(dateString).getJSDate()
}
```
https://github.com/bendrucker/postgres-date/blob/b3060560ed62c250f7800d29e4b68a9d5a0622bd/index.js#L285
# Calendar view mixing DATE and DATE_TIME
At the time of the refactor, DATE type wasn't properly tested with
calendar view, thus the drag and drop of a card with a calendar view on
a DATE field and not a DATE_TIME field, was not working, this is now
fixed.
## Changes
- Removed complex retry logic in `ImapClientProvider` by delegating to
root orchestrator
- Added `parseImapAuthenticationError` for handling revoked
authentication credentials previously the code existed in
`parse-imap-error.util` but it didn't belong there and nor was not used
in `ImapClientProvider` causing revoked channels to be stuck in limbo
- Removed `parseImapError` from `*-error-handler.service.ts `
- Created `isImapNetworkError` for consistency with Gmail and Microsoft
- `isImapNetworkError` is called at utility level for consistency
## Summary
This PR reduces clutter at the repository root to improve navigation on
GitHub. The README is now visible much sooner when browsing the repo.
## Changes
### Deleted from root
- `nx` wrapper script → use `npx nx` instead
- `render.yaml` → no longer used
- `jest.preset.js` → inlined `@nx/jest/preset` directly in each
package's jest.config
- `.prettierrc` → moved config to `package.json`
- `.prettierignore` → patterns already covered by `.gitignore`
### Moved/Consolidated
| From | To |
|------|-----|
| `Makefile` | `packages/twenty-docker/Makefile` (merged) |
| `crowdin-app.yml` | `.github/crowdin-app.yml` |
| `crowdin-docs.yml` | `.github/crowdin-docs.yml` |
| `.vale.ini` | `.github/vale.ini` |
| `tools/eslint-rules/` | `packages/twenty-eslint-rules/` |
| `eslint.config.react.mjs` |
`packages/twenty-front/eslint.config.react.mjs` |
## Result
Root items reduced from ~32 to ~22 (folders + files).
## Files updated
- GitHub workflow files updated to reference new crowdin config paths
- Jest configs updated to use `@nx/jest/preset` directly
- ESLint configs updated with new import paths
- `nx.json` updated with new paths
- `package.json` now includes prettier config and updated workspace
paths
- Dockerfile updated with new eslint-rules path
- new channel EVENT_STREAM_CHANNEL based on event stream id
- on event, perform the matching and publish only to the right streams
- store a list of active streams per workspace
- store the user id along with the queries for each stream
Bonus:
- remove onSubscriptionMatch
# Introduction
followup https://github.com/twentyhq/twenty/pull/16962
Root cause: as now the twenty standard app installation in seeded
workspace invalidates and set the page layout-xxx caches the inserted
through direct repo access data in the prefill legacy page layout
methods do not interact with the cache
The cache being set in prior when recompute does not result in db
introspection as before as when was unset
## Summary
The e2e login test was failing because it unconditionally tried to click
'Continue with Email' button, but this button doesn't exist when
password is the only auth method.
## Root Cause
In `SignInUpWorkspaceScopeFormEffect.tsx`, when a workspace only has
password authentication (no Google/Microsoft/SSO), the effect
automatically calls `continueWithEmail()` which skips the Init step and
shows the email field directly.
## Changes
1. **loginPage.ts**: Added `clickLoginWithEmailIfVisible()` method that
only clicks the button if it exists
2. **login.setup.ts**:
- Replaced `clickLoginWithEmail()` with `clickLoginWithEmailIfVisible()`
- Updated regex from `/Welcome to .+/` to `/Welcome, .+/` to match the
recent UI change
Fixes#17006
## Changes
Added `lastAuthenticateWorkspaceSsoMethodState` Recoil atom to track the
last used SSO method, persisted in localStorage
Updated `SignInUpWithGoogle` component to display a "Last" pill badge
when Google was the last auth method
Updated `SignInUpWithMicrosoft` component to display a "Last" pill badge
when Microsoft was the last auth method
Updated `SignInUpWithSSO` component to display a "Last" pill badge when
SSO was the last auth method.
The state is saved after successful authentication redirect, ensuring it
persists across sessions
## Implementation Details
Uses existing `localStorageEffect` for persistence across browser
sessions
Leverages the existing Pill component from twenty-ui with blue accent
color
The label is positioned on the right border of the button using absolute
positioning
State is saved in the `useAuth` hook during Google/Microsoft sign-in and
in the SSO login component
<img width="684" height="608" alt="image"
src="https://github.com/user-attachments/assets/629a1599-aab0-44e4-b684-ae91a8e725fd"
/>
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> Highlights the most recently used authentication method and preserves
it across sessions.
>
> - Introduces `AuthenticatedMethod` enum and
`lastAuthenticatedMethodState` (persisted via `localStorageEffect`) and
preserves it through `useAuth.clearSession`
> - Displays a "Last" pill via `LastUsedPill` on `SignInUpWithGoogle`,
`SignInUpWithMicrosoft`, `SignInUpWithSSO`, and
`SignInUpWithCredentials` when appropriate; adds
`StyledSSOButtonContainer` for badge positioning
> - Adds `useHasMultipleAuthMethods` to detect when to show the badge;
threads `isGlobalScope` to relevant components
> - Sets last-used method on click/submit in SSO and credentials flows
(`useSignInUp`, SSO button handlers)
> - Refactors `SignInUpGlobalScopeForm` to use `SignInUpWithCredentials`
and updates `SignInUp` title logic for global scope (e.g., "Welcome to
Twenty")
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
70d5527609. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Félix Malfait <felix@twenty.com>
- added article to detail how to attach a pdf to a given record
- added link in another article
- updated the english section of the docs.json to have the file appear
in the left menu
- updated the 2 navigation files
---------
Co-authored-by: github-actions <github-actions@twenty.com>
Refactors to be consistent with Microsoft service
Handles scenarios like temporary error which was not handled before
Moved `IsGmailNetworkError` from root orchestrator to driver level
Closes https://github.com/twentyhq/core-team-issues/issues/2058
We settled on a simpler solution by just hardcoding the objects order
for now. We will change this once it's possible to reorder the workspace
favorites with drag and drop.
# Introduction
Some legacy caches are based on the flat ones
So we need to seq invalidate before invalidating others as it could
result in inter dep cache invalidation race condition
# Introduction
As we've just
[identified](https://github.com/twentyhq/twenty/pull/16981) the standard
field metadata in production we can not enable the is unique comparison
even for standard entities
The current Japanese translations are severely lacking. My company
(based in Tokyo) uses Japanese as its main language. I've gone through
and corrected/standardized everything. This is good enough to merge as a
starting point.
Includes `lingui compile` for the changes.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> Major localization refresh focused on Japanese.
>
> - Rewrites and standardizes `ja-JP` message strings (clearer phrasing,
consistent terminology, corrected placeholders/punctuation)
> - Regenerates compiled Lingui locale bundles; updates `ja-JP.ts` and
`ko-KR.ts` outputs
> - No runtime logic changes; scope limited to generated i18n resources
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
3365dcdac6. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Félix Malfait <felix@twenty.com>
Upgraded to Storybook 10. We still use `@storybook/test-runner` for
testing since it appears it'd require more work to move from Jest to
Vitest than I initially anticipated, but I completed this PR to fix
`storybook:serve:dev` - it takes time to load, but it works the way it
used to with Storybook 8.
https://github.com/user-attachments/assets/7afc32c6-4bcf-4b37-b83b-8d00d28dda15
## Summary
- Update all illustration icons to use `theme.accent.accent3` for fill
and `theme.accent.accent8` for border
- Replaces hardcoded `IllustrationIcon.blue` values with
theme-responsive accent colors
- Icons will now adapt correctly to theme changes
## Test plan
- [ ] Navigate to Settings > Data model > select any object
- [ ] Verify the Relations "Type" column icons use the accent colors
- [ ] Verify the Fields "Data type" column icons use the accent colors
- [ ] Switch themes (if available) and verify icons adapt accordingly
# Introduction
Followup https://github.com/twentyhq/twenty/pull/16981
1/ Migration, applicationId and universalIdentifier are required on
entity ( save point migration + upgrade command fallback pattern )
2/ Backfill using previous standard ids
This PR is a follow-up of https://github.com/twentyhq/twenty/pull/16966
and implements a new mechanism to handle SSE events.
It creates only one event stream per browser tab, then use mutations to
tell the backend which query to listen to, without re-mounting the event
stream connexion.
Then each event that comes from this unique subscription is then
dispatched in a new JavaScript CustomEvent, per queryId, on which
specific hooks add an event listener.
This PR introduces the generic tooling as well as the handling of update
events on table.
## Summary
- Updated Relations and Fields tables in the object detail settings page
to use flexible width (`1fr`) for the Name column instead of fixed
pixels
- Aligned column widths between both tables (`1fr 148px 148px 36px`) so
the App and Type/Data type columns line up
## Test plan
- [ ] Navigate to Settings > Data model > select any object (e.g.,
Companies)
- [ ] Verify the Relations table rows take the full available width
- [ ] Verify the Fields table rows take the full available width
- [ ] Verify the App and Type/Data type columns are aligned between both
tables
## Summary
- Fixed the billing credits progress bar background color in dark mode
- The background was hardcoded to `BACKGROUND_LIGHT.tertiary`, causing
it to always display a light color regardless of the current theme
- Changed to use `theme.background.tertiary` to properly respect
dark/light mode
## Test plan
- [x] Navigate to Settings > Billing in dark mode
- [x] Verify the credits progress bar background matches the tertiary
background color (dark in dark mode)
- [x] Verify it still looks correct in light mode
Figma reference:
https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=64812-242606&m=dev
## Summary
This PR introduces row-level security (RLS) permissions for roles in the
frontend, allowing fine-grained access control at the record level.
Users can now define permission rules that determine which specific
records a role can access based on dynamic conditions and filters.
## What's Changed
Implemented UI for configuring record-level permissions on object
permissions screens
Added support for defining permission predicates using filter conditions
(similar to advanced filters)
Introduced variable picker for dynamic permission rules (e.g., "me"
context for user-specific access)
Built predicate conversion layer to sync UI state with backend
permission structure
Extended GraphQL schema with mutations for upserting row-level
permission predicates
Fixed handling of orphaned RLS groups to prevent data inconsistencies
Added enterprise key validation for RLS features
The implementation enables scenarios like "users can only see their own
records" or "users can access records associated with their team."
<img width="687" height="702" alt="Screenshot 2026-01-09 at 23 07 02"
src="https://github.com/user-attachments/assets/33fe736e-6cbf-40bd-b2eb-c8a90c8d21bc"
/>
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
This PR introduces a few changes:
- Add three actions: see deleted dashboards, destroy dashboard and
restore dashboard
- Remove the soft delete and restore on all the page layout entities
- Cascade the destruction of a dashboard to a page layout
Video QA:
https://github.com/user-attachments/assets/ab993b11-dd9c-4e88-880c-92691a521cc2
This PR refactors the table virtualization real index main state that
was creating performance problems.
The problem was that we kept track of all the real rows even if we only
print 200 at a time in the application.
This caused a crash when some users with 10k+ rows on a table tried to
iterate over this state 10.000 times or more.
The trade-off is that now we keep all real indices in a Map, and each
virtual row uses a selector to go to this Map.
This way if we want to reset the full Map, we just have to overwrite the
base state that contains the map, and the 200 selectors will recompute.
This happens for example when we delete a row. This now has a limited
and negligible performance impact.
## Summary
- Enable editing for custom calendar event fields while keeping standard
fields read-only
- Load calendar event fields dynamically so custom field values are
editable everywhere they appear
- Preserve calendar event participants rendering
## Testing
- npx nx run twenty-front:lint:diff-with-main --skip-nx-cache
--output-style=stream
- npx jest --config packages/twenty-front/jest.config.mjs
--runTestsByPath
packages/twenty-front/src/modules/activities/calendar/hooks/__tests__/useCalendarEvents.test.tsx
## Notes
- Full nx lint/test are very slow locally after barrel generation; CI
should run the full suite
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
# Introduction
fixes https://github.com/twentyhq/twenty/issues/16905
Do not merge until `IS_WORKSPACE_CREATION_V2_ENABLED` has been activated
by default, and so sync metadata has been deprecated by doing so. As the
sync metadata will attempt to insert `null` `applicationId` and
`universalIdentifier` values while creating a workspace
In this PR we're introducing a new `SyncableEntityRequired` which
enforces the non nullable `applicationId` and `universalIdentifier` on
extending entity
In this PR we also migrate the field metadata entity to extend the
required
## Identification upgrade command
This command will search for workspace field metadata entities that
aren't associated to an applicationId, dispatch them to either the
workspace-custom `applicationId` or the twenty-standard `applicationId`.
For the standard entities it will also set their universal identifier
based on the `STANDARD_OBJECTS` const hashmap
## Typeorm migration
As the non nullable `applicationId` and `universalIdentifier`migration
won't pass in the first we've been using the save point and upgrade
command migration fallback pattern
## Tests
Tested the command on a prod extract locally
Both `twenty-eng` and `twenty-for-twenty` have unexpected standard
objects
Please note that we will deprecate the `isCustom` and `standardId` col
later in the future
### Twenty-eng
```ts
[Nest] 98971 - 01/01/2026, 3:18:00 PM LOG [IdentifyStandardEntitiesCommand] Successfully validated 600/600 field metadata update(s) for workspace 9870323e-22c3-4d14-9b7f-5bdc84f7d6ee (309 custom, 291 standard)
[Nest] 98971 - 01/01/2026, 3:18:00 PM WARN [IdentifyStandardEntitiesCommand] Found 35 warning(s) while processing field metadata for workspace 9870323e-22c3-4d14-9b7f-5bdc84f7d6ee. These fields will become custom.
```
### Twenty for twenty
### Just created workspace
# Introduction
In this pull request we're introducing new standard page layout, tabs
and widget ( 1 page layout, 1 tab and 8 widgets ) and also a new
opportunity field
Also now prefilling new records, 6 opportunities and a dashboard.
## Standard declaration
### New workspace creation
Relies on existing standard declaration builder
### Backfill command
We've been hacking through the standard builder in order to extract only
the standard page layout entities, updated their entity dependencies to
match the workspace ids so the validation passes
## Remark
- Refactored the `PageLayoutWidget` configuration type to be dynamically
typed through a generic discriminated union
---------
Co-authored-by: prastoin <paul@twenty.com>
**Description**
Fixes the "Add new" button appearing in relation picker dropdowns for
workspace member relations (e.g., Account Owner on companies). Workspace
members cannot be created from relation pickers, so this button should
not appear.
**Changes Made**
- Modified RelationManyToOneFieldInput.tsx to conditionally pass the
onCreate prop to SingleRecordPicker only when
createNewRecordAndOpenRightDrawer is defined.
- Moved the conditional check to the JSX level for clarity.
**Technical Details**
The useAddNewRecordAndOpenRightDrawer hook already returns undefined for
workspace member relations, but the component was still passing a
handler function to SingleRecordPicker, causing the "Add new" button to
appear. The fix ensures that when createNewRecordAndOpenRightDrawer is
undefined, onCreate is also undefined, which prevents
SingleRecordPickerMenuItemsWithSearch from rendering the button.
Testing
- Verified that the "Add new" button no longer appears in the Account
Owner dropdown for companies.
- Confirmed that the "Add new" button still appears for other relation
fields where it's appropriate (e.g., People, Opportunities).
- Tested that existing functionality for selecting workspace members
from the dropdown still works correctly.
Fixes#17060
Created by Github action
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> Enhances Apps documentation across multiple locales by clarifying
object base fields behavior.
>
> - Adds a `Note` in localized `apps.mdx` pages (ar, cs, de, it, ro, ru,
tr) stating that standard base fields (e.g., `name`, `createdAt`,
`updatedAt`, `createdBy`, `position`, `deletedAt`) are created
automatically and should not be included in `fields` when using
`defineObject()`.
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
87a5ecd0b6. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Co-authored-by: github-actions <github-actions@twenty.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Upgraded from 8.6.15 to 9.1.17 in two steps:
- 8.6.15 -> 9.0.0
- 9.0.0 -> 9.1.17
I had to disable `storybook-addon-cookie` since it is not supported for
Storybook 9. However, I do intend to upgrade to Storybook 10 when this
is merged, so we can replace the aforementioned add-on with this fork
specifically created to support Storybook 10 and above:
https://www.npmjs.com/package/@storybook-community/storybook-addon-cookie.
Additionally, once we upgrade to Version 10 successfully, I will start
looking into integrating the official Vitest add-on.
## Description
Fixes a bug where queries with relation field + scalar field ordering
would fail with:
```
column distinctAlias.person_position does not exist
```
## Root Cause
When a GraphQL query orders by columns that are **not in the selected
fields**, TypeORM's DISTINCT subquery fails because it expects those
columns in the inner SELECT with alias format (e.g., `person_position`).
The issue only manifests when:
1. A filter is applied (triggers DISTINCT path in TypeORM)
2. OrderBy includes columns NOT in the GraphQL selection
3. Example: query selects only `id`, but orders by `position`
## The Fix
`addRelationOrderColumnsToBuilder` now accepts `columnsToSelect` and
adds orderBy columns via `addSelect()` only if they're **NOT** already
in the selected columns:
- **Relation orderBy columns**: Always added (never in columnsToSelect)
- **Main entity orderBy columns**: Added only when not already selected
This ensures all orderBy columns are present in TypeORM's inner SELECT
for the DISTINCT subquery.
## Test Plan
Added integration test for the exact failing scenario:
- Filter with `neq` (triggers DISTINCT path)
- Multiple orderBy: relation field + scalar field
- Minimal field selection (only `id`, not `position`)
## Related
Regression introduced in #17021 which added nested sort support.
## Summary
- Add `additionalEmails` (EMAILS type) to tsvector search expression
- Add `additionalPhones` (PHONES type) to tsvector search expression
- Add `secondaryLinks` (LINKS type) to tsvector search expression
This enables searching for people/companies by their secondary contact
information, not just primary values.
## Test plan
- [x] Unit tests for all three composite field types (16 tests passing)
- [x] Integration tests for searching by secondary email, work email,
partial domain
- [x] Integration tests for searching by additional phone numbers
- [x] Integration test for searching by secondary link URL
- [x] Lint passes
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
- moved a few gql types to twenty shared to re-use in server
- added a new endpoint, onEventSubscription that expect a streamId to
create a connection
- two new endpoints to store queries in Redis
- updated the existing batch channel to directly use object record type
Closes#17032
Summary
When a sort is already applied to a column, clicking "Sort" from the
column header dropdown now toggles the sort direction (ASC → DESC or
DESC → ASC) instead of doing nothing.
Problem
Previously, clicking "Sort" on a column that already had a sort applied
would always try to create a new sort with ASC direction. Since the
upsertRecordSort function updates an existing sort with the same field,
this effectively did nothing when the sort was already ASC—which felt
unintuitive.
Solution
Modified useHandleToggleColumnSort to:
- Check if a sort already exists for the clicked field
- If it exists, toggle the direction
- If it doesn't exist, create a new sort with ASC direction (existing
behavior)
---
Contribution by Gittensor, see my contribution statistics at
https://gittensor.io/miners/details?githubId=132382032
## Context
Text fields were being sorted case-sensitively on the backend (e.g.,
'Apple', 'apple', 'Banana' would sort as 'Apple', 'Banana', 'apple').
This resulted in unexpected sorting behavior that differed between the
frontend Apollo cache sorting and backend database sorting.
## Changes
### Backend (packages/twenty-server)
- **`graphql-query-order.parser.ts`**:
- Added `shouldUseCaseInsensitiveOrder()` helper that returns `true` for
TEXT, SELECT, and MULTI_SELECT fields
- Added `buildOrderByColumnExpression()` method that wraps column
expressions with `LOWER()` for case-insensitive sorting
- Updated `parse()` and `parseObjectRecordOrderByForScalarField()` to
use the new helpers
- Updated `parseObjectRecordOrderByForRelationField()` to apply LOWER()
to nested text fields
- **`parse-composite-field-for-order.util.ts`**:
- Added `shouldUseCaseInsensitiveOrder()` helper for composite subfields
- Updated composite field parsing to apply `LOWER()` to subfields of
type TEXT (e.g., `name.firstName`, `name.lastName`)
### Frontend (packages/twenty-front)
- **`sort.ts`**:
- Updated `sortAsc()` to use case-insensitive comparison
(`toLowerCase()`) for string values
- `sortDesc()` automatically benefits from this since it delegates to
`sortAsc()`
- Ensures Apollo cache sorting matches backend behavior
## Example
Before:
```sql
ORDER BY "person"."name" ASC
```
Result: ['Apple', 'Banana', 'apple', 'cherry']
After:
```sql
ORDER BY LOWER("person"."name") ASC
```
Result: ['apple', 'Apple', 'Banana', 'cherry']
## Testing
- Manual testing of sorting on People and Companies views
- Verified frontend cache sorting matches backend results
## Summary
This PR enables sorting records by fields of related objects. For
example, sorting **People by their Company's name**.
### Before
Only scalar and composite fields could be sorted. Relation fields showed
in the sort dropdown but produced errors.
### After
Many-to-One relation fields can now be sorted using the related object's
**label identifier field** (e.g., Company's `name`).
---
## Changes
### Frontend
- Added `RELATION` to sortable field types (restricted to `MANY_TO_ONE`
relations)
- New `getOrderByForRelationField()` generates nested orderBy structures
using the related object's label identifier
- Updated `turnSortsIntoOrderBy()` to handle relation fields by looking
up related object metadata
### Backend
- Extended `GraphqlQueryOrderFieldParser.parse()` to detect nested
relation ordering like `{ company: { name: 'AscNullsLast' } }`
- Returns `ParseOrderByResult` containing both `orderBy` conditions and
`relationJoins` info
- Added LEFT JOINs for relation ordering in `applyOrderToBuilder()`
- Added `addRelationOrderColumnsToBuilder()` for TypeORM DISTINCT
compatibility
### Tests
- Added unit tests for `filterSortableFieldMetadataItems`,
`getOrderByForRelationField`, and `turnSortsIntoOrderBy`
- Added integration tests covering ascending/descending order and
composite label identifiers
---
## TypeORM Bug Workaround
We encountered a significant TypeORM limitation when implementing this
feature. When using `getMany()` with `ORDER BY` on joined relation
columns, TypeORM generates a DISTINCT subquery that has specific
requirements:
### Issue 1: Alias Parsing
TypeORM's `orderBy()` method fails with **"alias not found"** when using
quoted SQL identifiers like `"company"."name"`. TypeORM internally
expects unquoted property paths (e.g., `company.name`) for its alias
resolution mechanism.
### Issue 2: setFindOptions Clears addSelect
`setFindOptions({ select })` **clears any previously added `addSelect()`
columns**. This caused `"column distinctAlias.company_name does not
exist"` errors because the relation columns needed for ORDER BY were
being removed.
### Solution
We split the logic into two methods:
1. `applyOrderToBuilder()` - adds JOINs and ORDER BY (before
`setFindOptions`)
2. `addRelationOrderColumnsToBuilder()` - adds relation columns for
SELECT (AFTER `setFindOptions`)
This ensures the relation columns are present in the final SQL query's
SELECT clause with the proper underscore aliases (`company_name`) that
TypeORM's DISTINCT subquery expects.
**Related TypeORM issue**:
https://github.com/typeorm/typeorm/issues/9921
---
## Screenshots/Demo
_Add screenshots if applicable_
Previous behavior:
Used billingSubscription.updatedAt (when the subscription record was
last modified)
Problem: updatedAt changes for ANY update, not just status changes,
making it unreliable for tracking when payment problems started
Current behavior:
Uses currentPeriodStart when subscription status is Unpaid or Canceled
Why it works: WORKSPACE_INACTIVE_DAYS_BEFORE_SOFT_DELETION is shorter
than the minimum billing interval (1 month). Then if a workspace is
unpaid for the entire current billing period, it will always exceed the
deletion threshold before the next period starts
Ideal fix:
Track workspace.suspendedAt explicitly, giving you the exact timestamp
of when payment problems began, regardless of billing periods.
`MessageImportExceptionHandlerService.handleSyncCursorErrorException()`
was calling `messageChannelSyncStatusService.markAsFailed()`
instead of
`messageChannelSyncStatusService.resetAndMarkAsMessagesListFetchPending()`
Also adds a new command `MessagingTriggerMessageListFetchCommand` for
faster local development speed
## Summary
Fixes#16900 - MS Office documents (doc, docx, ppt, pptx, xls, xlsx,
odt) cannot be previewed when hosted on local/private networks.
## Problem
Microsoft's Office Online viewer (`view.officeapps.live.com`) requires
documents to be publicly accessible from the internet. For self-hosted
Twenty instances or local development, files are not reachable by
Microsoft's servers, causing the viewer to fail with "An error
occurred".
## Solution
Detect private/local URLs upfront and show a helpful message with a
download button instead of letting the viewer fail silently.
**Detected private URLs:**
- `localhost`, `127.0.0.1`, `[::1]`
- Private IP ranges: `10.x.x.x`, `172.16-31.x.x`, `192.168.x.x`
- Local domain patterns: `.local`, `.localhost`, `.internal`
## Alternatives Explored (that don't work)
We investigated more sophisticated detection approaches:
1. **postMessage events** - Microsoft's viewer doesn't send any error
messages we can listen to
2. **Fetching the embed URL** - Blocked by CORS (Microsoft doesn't set
`Access-Control-Allow-Origin`)
3. **Reading iframe content** - Cross-origin restrictions prevent
JavaScript access
The URL-based detection is the most reliable approach for catching the
common cases where preview will definitely fail.
## Testing
1. Upload an Office file (docx, pptx, xlsx) on a local Twenty instance
2. Try to preview it
3. Should see "This file cannot be previewed because it is hosted
locally" with a Download button
Fixes : https://github.com/twentyhq/twenty/issues/17035
There was a bad pattern which risked introduce this bug in
`turnRecordFilterIntoRecordGqlOperationFilter` after recent refactor of
date logic and date filters, which didn't create any bug during dev time
because it wasn't tested with value combinations that existed before the
refactor.
Code has been re-organized to make it resilient to any wrong value
combination.
Also fixed a small bug that appeared during QA with `DATE` filter type,
which was blocking the save button from disappearing after a save.
# Introduction
Followup of
https://github.com/twentyhq/twenty/pull/17001#pullrequestreview-3638508738
close https://github.com/twentyhq/core-team-issues/issues/1910
We've completely decom the `sync-metadata` in production. We're now then
removing its implementation in favor of the v2.
## TODO:
- [x] Remove sync-metadata implem and commands
- [x] Remove workspace decorators
- [x] Type each deprecated field to deprecated on their workspaceEntity
- [x] Remove the `workspace-sync-metadata` folder entirely
- [x] remove workspace migration
- [x] workspace migration removal migration
- [x] remove the `v2` references from workspace manager file names
- [x] remove the `v2` references from workspace manager modules
- [ ] Double check impact on translation file path updates
## Note
- Removed the gate logic
- Remains some service v2 naming, serverless needs to be migrated on v2
fully
- Removed workspaceMigration service app health consumption, making it
always returning up ( no more down ) cc @FelixMalfait ( quite obsolete
health check now, will require complete refactor once we introduce inter
app dependency etc )
I introduced a bug in the chart sorting in this PR:
https://github.com/twentyhq/twenty/pull/16794
This PR fixes this.
The sorting on the first axis is already done by the group by for
FIELD_ASC and FIELD_DESC. It is sorted also for the second axis but in
each group.
For instance, if I create a graph that displays the amount of sales on
each day of the week grouped by vendor
I can have for:
Monday: Vendor B, Vendor C
Tuesday: Vendor A, Vendor B
Wednesday: Vendor A, Vendor C
...
Inside each day the order of the vendor is correct, but by looking at
only one day, I can't know the order of all the vendors.
That is why we always need to sort the second axis.
# Add Helm Chart
- Introduces a Twenty Helm chart with sensible defaults: internal
Postgres/Redis, auto DB creation/user, migrations, TLS via cert-manager,
and quickstart docs.
## Feedback requested
- Handling replicas > 1 with local storage (warn/force S3?).
- Defaults/guards for ephemeral pods + S3.
- Axis labels now respect `axisNameDisplay` config when chart has no
data (previously always showed both) - fixed for bar and line charts
- Horizontal bar charts now correctly map axis settings to visual
positions:
- "X axis" setting → bottom axis (value)
- "Y axis" setting → left axis (category)
video QA
https://github.com/user-attachments/assets/5bf32294-9a5e-4aa6-b3fd-0d4e33608d14
### Problem:
If the command menu page is kept opened while navigating to record show
page then the app crashes.
### Root cause
When navigatiing to a record show page while the command menu page is
kept opened, it tries to open the record show page with command menu
open, as its state were set to true , before navigating to a new page
along with the instance id of previous context which are used in the
current context while rendering the page. Hence, leading to an error
page.
### Changes
while navigation to a record show page the command menu open state is
set to false. This is to handle the navigation gracefully
Fixes: #16577
## Context
The feature has not been maintained for more than a year and was never
officially launched.
This PR removes its code due to the upcoming refactoring of the
sync-metadata that will break its logic if not handled properly and it
would be too costly for the time being.
TODO:
- remove isRemote from object/field metadata
- Fix groupBy field matching for composite fields nested within relation
fields
- Add `nestedSubFieldName` comparison when the nested field is a
composite type (CURRENCY, FULL_NAME, ADDRESS, etc.)
For huge workflows, after 20 steps, we stop the job and re-trigger a new
one.
But some of those workflows actually never stop.
See https://github.com/twentyhq/private-issues/issues/401
Here is a first issue. We resuming the workflow, we were providing the
result instead of the output
- Add widget validation
- Remove 'None' option for primary axis group by
- Fix error message parsing by passing the operation type in
`useMetadataErrorHandler`
- more details on rate at which messages are imported from Gmail
- Settings -> Release deprecated to Settings -> Updates => this leads to
updating the file title, file name and the navigation files
- Support & Documentation accessible via Settings and no longer the nav
bar
- updated features out of the lab
- no more integration page
- added a new article with step by step on how to notify a teammate of a
note to review
docs.json is updated only for the English part
---------
Co-authored-by: github-actions <github-actions@twenty.com>
**Bug Fixed**
Creating a new record in the People table view caused a browser memory
crash ("Paused before potential out of memory crash").
**Root Cause**
In useResetVirtualizationBecauseDataChanged.ts, when creating a record,
a loop iterated from 0 to totalNumberOfRecordsToVirtualize (the total
database count).
**Fix Applied**
Changed the loop to only iterate through indices from actually loaded
pages
Fixes https://github.com/twentyhq/twenty/issues/16980
Fixes an issue where column width changes and other view field
modifications were not persisted, and a toast error was shown.
**Root Causes:**
1. Frontend was using client-generated IDs instead of database IDs for
updates
2. Backend cache was stale, causing `View field to update not found`
error
**Changes:**
- Use the existing database ID when updating view fields
- Exclude client-generated IDs when creating new view fields
- Invalidate flat entity maps cache before/after view field operations
- Refresh both view and view field caches to prevent validation errors
Fixes#16417Closes#16381
Fixes [#16819](https://github.com/twentyhq/twenty/issues/16819)
Added a post-query hook (NoteDeleteOnePostQueryHook) that automatically
soft-deletes all associated noteTarget records when a note is deleted.
## Key changes:
Created `note-delete-one.post-query.hook.ts`
Uses `GlobalWorkspaceOrmManager` to access workspace entities correctly
Executes in the proper workspace context to ensure data isolation
Soft-deletes noteTarget records to maintain referential consistency
## Testing
1. Company notes
Create a Company
Create and attach a Note to the Company
Navigate to Company → Notes page (should display the note)
Delete the Note
Navigate to Company → Notes page (should no longer crash)
Restore the Note (should appear again)
Delete and destroy the Note permanently
Navigate to Company → Notes page (should still work, just empty)
2. Opportunity notes
Create an Opportunity
Create and attach a Note to the Opportunity
Navigate to Opportunity → Notes page (should display the note)
Delete the Note
Navigate to Opportunity → Notes page (should no longer crash)
Restore the Note (should appear again)
Delete and destroy the Note permanently
Navigate to Opportunity → Notes page (should still work, just empty)
### Expected behavior:
Company Notes page remains functional at all stages
No console errors
Deleted notes properly excluded from the view
Other notes on the same Company remain accessible
Additional Notes
This fix ensures data consistency by maintaining the relationship
between notes and their targets throughout the deletion lifecycle.
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
Closes#16792
## Problem
When editing a currency field’s default value (for example, changing
from **USD** to **UYU**), the **Save** button remained disabled.
However, if the format setting was changed first (**short → full →
short**), the Save button would then work correctly for currency
changes.
## Root Cause
The form was using **React Hook Form’s `values` mode**, which did not
properly track dirty state for the `defaultValue` and `settings` fields.
## Solution
Switched to the **`defaultValues` + `reset()`** pattern:
- Initialize the form with `defaultValues` (using placeholder values)
- Call `reset()` when `fieldMetadataItem` loads
This correctly tracks dirty state by comparing against the most recent
`reset` values.
## Note
A similar pattern is used in:
- `useWebhookForm`
- `useImapSmtpCaldavConnectionForm`
Those hooks call `reset()` via query callbacks. In this case, `reset()`
is called inside a `useEffect` because the data comes from synchronous
Recoil state rather than an async query.
Found two solutions to the problem, created commits for both. I feel the
useEffect pattern makes more sense here since it's cleaner in terms of
code.
---------
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Since tests are now run in the pre-merge queue with the latest main
version, they need not to be run again when merged into main, it would
be the exact same thing
Fixes https://github.com/twentyhq/twenty/issues/16928 and
https://github.com/twentyhq/private-issues/issues/400
When an object is renamed, its corresponding relation field on
noteTarget and taskTarget are not renamed. This caused an issue as in
the FE, we used the related record object name to compute the field name
used in the payload.
This PR addresses it by, instead, finding the field name after
identifying it thanks to the object id.
To reproduce, you can rename an object then try to tie it to a note.
- **nx-rules.mdc** - Nx workspace guidelines and best practices (Auto-attached to Nx files)
- **server-migrations.mdc** - Backend migration and TypeORM guidelines for `twenty-server` (Auto-attached to server entities and migration files)
- **creating-syncable-entity.mdc** - Comprehensive guide for creating new syncable entities (with universalIdentifier and applicationId) in the workspace migration system (Agent-requested for metadata-modules and workspace-migration files)
### Code Quality
- **typescript-guidelines.mdc** - TypeScript best practices and conventions (Auto-attached to .ts/.tsx files)
@@ -39,6 +40,7 @@ You can manually reference any rule using the `@ruleName` syntax:
- `@nx-rules` - Include Nx-specific guidance
- `@react-general-guidelines` - Load React best practices
- `@testing-guidelines` - Get testing recommendations
- `@creating-syncable-entity` - Guide for creating new syncable entities
### Rule Types Used
- **Always Applied** - Loaded in every context (architecture.mdc, README.mdc)
@@ -53,7 +55,7 @@ You can manually reference any rule using the `@ruleName` syntax:
BODY="⚠️ Claude ran out of turns before creating a PR. Work has been pushed to [\`$BRANCH\`](https://github.com/${{ github.repository }}/tree/$ENCODED_BRANCH).\n\n[**Create PR →**]($PR_URL)"
prompt = `You are responding to a comment on issue #${p.issue_number} ("${p.issue_title}") in the ${p.repo_full_name} repository.\n\nThe comment by @${p.sender} says:\n\n${p.comment_body}\n\nIssue body:\n\n${p.issue_body}\n\nPlease help with this request. The code you are working with is the twenty codebase (this repository).`;
} else {
prompt = `You are responding to issue #${p.issue_number} ("${p.issue_title}") in the ${p.repo_full_name} repository, opened by @${p.sender}.\n\nIssue body:\n\n${p.issue_body}\n\nPlease help with this request. The code you are working with is the twenty codebase (this repository).`;
IMPORTANT: Use Context7 for code generation, setup or configuration steps, or library/API documentation. Automatically use the Context7 MCP tools to resolve library IDs and get library docs without waiting for explicit requests.
### Before Making Changes
1. Always run linting and type checking after code changes
2. Test changes with relevant test suites
3. Ensure database migrations are properly structured
1. Always run linting (`lint:diff-with-main`) and type checking after code changes
2. Test changes with relevant test suites (prefer single-file test runs)
3. Ensure database migrations are generated for entity changes
4. Check that GraphQL schema changes are backward compatible
5. Run `graphql:generate` after any GraphQL schema changes
### Code Style Notes
- Use **Emotion** for styling with styled-components pattern
- Follow **Nx** workspace conventions for imports
- Use **Lingui** for internationalization
-Components should be in their own directories with tests and stories
-Apply security first, then formatting (sanitize before format)
### Testing Strategy
- **Unit tests** with Jest for both frontend and backend
- **Integration tests** for critical backend workflows
-**Storybook** for component development and testing
-**E2E tests** with Playwright for critical user flows
- **Test behavior, not implementation** — focus on user perspective
- **Test pyramid**: 70% unit, 20% integration, 10% E2E
-Query by user-visible elements (text, roles, labels) over test IDs
-Use `@testing-library/user-event` for realistic interactions
- Descriptive test names: "should [behavior] when [condition]"
- Clear mocks between tests with `jest.clearAllMocks()`
## CI Environment (GitHub Actions)
When running in CI, the dev environment is **not** pre-configured. Dependencies are installed but builds, env files, and databases are not set up.
- **Before running tests, builds, lint, type checks, or DB operations**, run: `bash packages/twenty-utils/setup-dev-env.sh`
- **Skip the setup script** for tasks that only read code — architecture questions, code review, documentation, etc.
- The script is idempotent and safe to run multiple times.
## Important Files
-`nx.json` - Nx workspace configuration with task definitions
-`tsconfig.base.json` - Base TypeScript configuration
-`package.json` - Root package with workspace definitions
-`.cursor/rules/` - Development guidelines and best practices
-`.cursor/rules/` - Detailed development guidelines and best practices
Create Twenty App is the official scaffolding CLI for building apps on top of [Twenty CRM](https://twenty.com). It sets up a ready‑to‑run project that works seamlessly with the [twenty-sdk](https://www.npmjs.com/package/twenty-sdk).
- Zero‑config project bootstrap
- Preconfigured scripts for auth, generate, dev sync, one‑off sync, uninstall
- Preconfigured scripts for auth, dev mode (watch & sync), generate, uninstall, and function management
- Strong TypeScript support and typed client generation
## Documentation
See Twenty application documentation https://docs.twenty.com/developers/extend/capabilities/apps
## Prerequisites
- Node.js 24+ (recommended) and Yarn 4
- A Twenty workspace and an API key (create one at https://app.twenty.com/settings/api-webhooks)
@@ -28,29 +31,33 @@ Create Twenty App is the official scaffolding CLI for building apps on top of [T
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# If you don't use yarn@4
corepack enable
yarn install
# Get help
yarn run help
# Authenticate using your API key (you'll be prompted)
yarn auth
yarn auth:login
# Add a new entity to your application (guided)
yarn create-entity
yarn entity:add
# Generate a typed Twenty client and workspace entity types
yarn generate
yarn app:generate
# Start dev mode: automatically syncs local changes to your workspace
yarn dev
# Start dev mode: watches, builds, and syncs local changes to your workspace
Internal PostgreSQL credentials are managed by the chart and not exposed by default. If you need direct access, create your own user in the database pod or use an external PostgreSQL instance.
Jobs for DB creation and migrations have been removed to simplify deployments; the server handles readiness and migrations at startup.
Deploy Twenty CRM on Kubernetes with server, worker, PostgreSQL, and Redis components.
## Features
- Server and worker deployments with full env exposure via `values.yaml`.
- Internal PostgreSQL (Spilo) and Redis deployments included.
- PVC-based persistence using dynamic storage classes (no static PV manifests).
- Ingress with configurable annotations, hosts, and TLS.
- Database readiness and migrations handled by server/worker init containers by default.
– Standard Kubernetes Jobs for DB creation/user and migrations have been removed to simplify installs. Readiness and migrations run in init containers.
## Quick Start
See [QUICKSTART.md](QUICKSTART.md) for a simple 2-line install with your domain.
**S3:** Set `storage.type=s3` and provide credentials using a values file. You can either pass credentials directly or reference an existing Kubernetes Secret.
- **Image versioning:** The chart defaults to `Chart.yaml`'s `appVersion` (currently v1.14.0). Override via `image.tag` in values to pin a different version or use `latest` for rolling updates.
- **Keep secrets secure:** Avoid `--set` for sensitive values; use `-f values-secrets.yaml` or reference existing Kubernetes Secrets via `server.extraEnvFrom`.
- S3 credentials can be referenced via `storage.s3.secretName + accessKeyIdKey/secretAccessKeyKey` to avoid embedding them in pod specs.
- If ingress disabled: expose the service as needed (e.g., `kubectl port-forward svc/{{ include "twenty.fullname" . }}-server 3000:{{ .Values.server.service.port }}` or create a LoadBalancer/NodePort).
Configuration:
- Using internal DB: {{ ternary "yes" "no" .Values.db.enabled }}
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = :'app_user') THEN
EXECUTE format('CREATE USER %I WITH PASSWORD %L', :'app_user', :'app_password');
END IF;
END
$do$;
EOSQL
echo "Creating core schema and granting permissions..."
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d "${DBNAME}" -v app_user="${APP_USER}" -c 'CREATE SCHEMA IF NOT EXISTS core'
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d "${DBNAME}" -v db="${DBNAME}" -v app_user="${APP_USER}" -c 'GRANT ALL PRIVILEGES ON DATABASE :"db" TO :"app_user";'
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d "${DBNAME}" -v app_user="${APP_USER}" -c 'GRANT ALL PRIVILEGES ON SCHEMA core TO :"app_user";'
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d "${DBNAME}" -v app_user="${APP_USER}" -c 'GRANT ALL PRIVILEGES ON SCHEMA public TO :"app_user";'
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d "${DBNAME}" -v app_user="${APP_USER}" -c 'GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA core TO :"app_user";'
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d "${DBNAME}" -v app_user="${APP_USER}" -c 'GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO :"app_user";'
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d "${DBNAME}" -v app_user="${APP_USER}" -c 'ALTER DEFAULT PRIVILEGES IN SCHEMA core GRANT ALL ON TABLES TO :"app_user";'
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d "${DBNAME}" -v app_user="${APP_USER}" -c 'ALTER DEFAULT PRIVILEGES IN SCHEMA core GRANT ALL ON SEQUENCES TO :"app_user";'
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d "${DBNAME}" -v app_user="${APP_USER}" -c 'ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO :"app_user";'
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d "${DBNAME}" -v app_user="${APP_USER}" -c 'ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO :"app_user";'
echo "Database ${DBNAME} is ready."
{{- end }}
- name:run-migrations
{{- $img := include "twenty.server.image" . }}
image:{{include "twenty.image.repository" $img }}:{{ include "twenty.image.tag" $img }}
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.