Before - workflow run not up to date, needs refresh to see created
company in some cases
https://github.com/user-attachments/assets/28517e97-2404-4f75-8bce-cc33e3cbea20
After
https://github.com/user-attachments/assets/60f930cb-1265-4c50-8ec5-aa4f978b1873
## Summary
- Split `SSEQuerySubscribeEffect`'s single debounced
`updateQueryListeners` into separate `syncAdditions` (leading edge, 1s
debounce) and `syncRemovals` (trailing edge, 200ms debounce) callbacks.
This prevents query unregistrations during component mount/unmount
transitions from creating gaps where events are missed, while keeping
new registrations immediate.
- Each sync path now updates `activeQueryListenersState` granularly
(append-only for additions, filter-only for removals) instead of
overwriting the entire state, eliminating a race condition where
removals could mark unregistered queries as active.
- Mount `WorkflowRunSSESubscribeEffect` inside
`WorkflowEditActionFormFiller` so the workflow-run query subscription
stays active during form steps.
- Extract `buildSortedConnectionEdges` util that builds the resulting
edge list of a cached record connection after new records are created.
Position placeholders (`'first'` / `'last'`) bypass orderBy and are
pinned to the front/back; sortable positions (numeric or undefined) are
merged into existing edges and sorted by the connection's actual
`orderBy`. This replaces the broken `length * position` insertion logic
in `triggerCreateRecordsOptimisticEffect` that treated the sortable
`position` field as a 0-1 ratio, causing new records from SSE to land at
invisible indices in the cached list. Also fixes `totalCount` increment
for batched creates, derives `pageInfo` cursors from the final array,
and gracefully skips records whose `toReference` returns null.
## Test plan
- [x] Run a workflow with a form step — verify the workflow status
updates live after form submission (no stuck "running" state)
- [x] Run the same workflow multiple times — verify company creation
events appear live on the record index page for every run, not just the
first
- [x] Click the "+" button to create a record in first position — verify
it appears immediately at the top
- [x] Verify other SSE-backed live updates (record creation, deletion,
updates) still work correctly
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
#### SEO
- Heading default flipped from h1 → h2; only Hero.Heading defaults to
h1. Eliminates accidental multi-h1 pages, which was confusing search
engines about the primary topic.
- Titles and descriptions in static-website-routes.ts rewritten to be
keyword-led and unique per page.
- Added buildFaqPageJsonLd (used on /, /pricing) and
buildReleaseListJsonLd (used on /releases).
- ReleaseEntry now renders id={release} so JSON-LD @id fragments resolve
to anchors.
#### Footer language switcher
- New LocaleSwitcher.tsx (plain React popover — useState + useRef +
outside-click). Trigger renders globe icon + native language name
(Français); popover lists all enabled locales with native + English
names side-by-side.
- Intl.DisplayNames-based name resolution in locale-display-names.ts.
- Plumbed into the footer's bottom row next to copyright.
Translations have not been pulled from Crowdin yet, so French pages
currently show English copy.
# Introduction
Running `yarn workspace focus twenty`( only installing root package.json
dependencies ) would fail because the yarn constraint expect the yarn
types to be installed
## Summary
- Add `defineCommandMenuItem` and `definePageLayoutWidget` as standalone
SDK defines, mirroring the existing `definePageLayoutTab` pattern. Both
entities can still be declared nested inside their parent
(`defineFrontComponent.command` / `definePageLayout.tabs[].widgets[]`).
- Add `CommandMenuItem` and `PageLayoutWidget` to the `SyncableEntity`
enum and the dev-mode UI labels.
- Wire the SDK manifest-build to extract the two new defines into
top-level `commandMenuItems` / `pageLayoutWidgets` arrays on the
manifest, and the server aggregator to consume them through the existing
flat-entity converters.
- On the server, expose `Application.commandMenuItems` (relation + DTO +
service hydration in `findOneApplication`).
- On the front, list command menu items in the application content tab
and add a dedicated detail page with a settings tab, mirroring how
`frontComponents` are surfaced.
- Add `twenty add` templates and Vitest unit tests for both new defines.
- Document the standalone-vs-nested pattern in
`packages/twenty-sdk/README.md`.
### Why
Until now, command menu items could only be declared as the nested
`command:` field on `defineFrontComponent` — there was no way to
register a command menu item from a separate file or from another
package. The `SyncableEntity` enum had 12 values, while the server
already synced 18 (including `commandMenuItem` and `pageLayoutWidget`).
The same gap existed for `pageLayoutWidget`, which had no top-level
define despite being synced server-side. This PR closes both gaps and
aligns the SDK surface with what the server actually accepts.
The standalone defines coexist with the nested form — pick one per
entity, never both with the same `universalIdentifier` (the manifest
aggregator will throw on duplicates). The README now documents this.
## Test plan
- [x] `npx nx typecheck twenty-sdk` / `twenty-server` / `twenty-front`
- [x] `npx nx lint:diff-with-main twenty-front` / `twenty-server`
- [x] `npx nx lint twenty-sdk` / `twenty-shared`
- [x] New unit tests: `define-command-menu-item.spec.ts`,
`define-page-layout-widget.spec.ts`
- [x] Existing manifest extract config tests still pass
- [ ] Codegen `npx nx run twenty-front:graphql:generate
--configuration=metadata` should be re-run after merge — the generated
`graphql.ts` was patched manually to include `commandMenuItems` on
`Application` and the `FindOneApplication` document.
- [ ] Smoke test: scaffold an app with `twenty add` for both new entity
types, run `twenty dev`, confirm the dev UI shows them in the sync list
and the settings page surfaces command menu items in the content tab.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: martmull <martmull@hotmail.fr>
Co-authored-by: Charles Bochet <charles@twenty.com>
Original CalDAV driver was written almost a year ago and code quality,
patterns were not up to the mark including having no test coverage, this
PR does the following:
- Splits the monolithic driver into isolated utilities with test
coverage
- Adds support for syncing legacy servers by checking if server supports
`syncCollection` and branches into two sync methods
`fetchEventsViaSyncCollection` or `fetchEventsViaCtagEtag` with this I
believe our driver is feature complete
Real testing report
| Provider | Server | Sync method | Auth |
| --------- | ----------------- | -------------------- | ------ |
| iCloud | Apple's CalDAV | sync-collection | Basic |
| Nextcloud | sabre/dav | sync-collection | Basic |
| all-inkl | sabre/dav (older) | ctag + etag fallback | Digest |
## Summary
The new `CI Website` workflow added in #20281 fails on the `test` matrix
job because tests cannot resolve `twenty-shared/translations` — a
subpath that requires `twenty-shared` to be built first.
Root cause: `packages/twenty-website-new/project.json` fully overrides
the `test` target, duplicating the executor/options/configurations from
`nx.json` `targetDefaults` but **losing `dependsOn: ["^build"]`** (and
`inputs` / `cache`). As a result, `nx affected -t test` for
`twenty-website-new` does not build `twenty-shared` first.
`twenty-front` works because its `project.json` declares `"test": {}`
and inherits the full default. This PR does the same for
`twenty-website-new`.
Verified the diagnosis from the failing run — `front-task (test)` logs
show `nx run twenty-shared:build` is invoked transitively, while
`website-task (test)` logs do not, leading to the missing-module error.
This was a leftover column removed in
https://github.com/twentyhq/twenty/pull/6743 but was accidentally added
again when we migrated to `buildMessageStandardFlatFieldMetadatas` from
workspace decorator
/closes #20011
## Summary
The current Docker-not-running message is unhelpful in two ways:
1. It doesn't tell users **how** to start Docker
2. "try again" is meaningless because a first-time user doesn't yet know
the command they just ran (they got here from `create-twenty-app`, not
from typing `yarn twenty server start` themselves)
**Before:**
```
Docker is not running. Please start Docker and try again.
```
**After (macOS example):**
```
Docker is not running.
Start Docker:
Run: open -a Docker
(or launch Docker Desktop from Applications)
Then retry:
yarn twenty server start
Don't have Docker? Install from https://docs.docker.com/get-docker/
```
The platform-specific line is detected via `process.platform`:
- `darwin` → `open -a Docker` + Docker Desktop fallback
- `linux` → `sudo systemctl start docker` + Docker Desktop fallback
- `win32` → "Launch Docker Desktop from the Start menu"
- other → link to install docs
The retry command is computed at the call site so it preserves the
user's actual flags — `yarn twenty server start --test`, `yarn twenty
server upgrade 2.2.0 --test`, etc.
## Why
This came out of shadowing a first-time app developer who hit this error
during `npx create-twenty-app`. They were stuck — the CLI told them to
"try again" but they had only learned two commands so far
(`create-twenty-app` and `yarn dev`), neither of which was the right
one. Improving the message turns the error into a teaching moment.
## Test plan
- [x] `npx nx typecheck twenty-sdk` passes
- [x] `npx nx lint twenty-sdk` passes
- [x] Manually verified rendered output for both `server start` and
`server upgrade` flows on macOS
- [ ] Verify message renders correctly on Linux/Windows in practice
## Possible follow-ups (out of scope)
- Auto-launch Docker Desktop on macOS if installed (changes user state —
separate PR)
- Make the multi-line CLI error printer style only the first line in
red, so guidance reads as default text rather than red
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Summary
Restructures the apps Getting Started doc around the three things a
developer actually has to do, so the mental model is visible upfront and
discoverable when something goes wrong.
**Why this matters:** the previous flow read as one continuous list of
bash commands and prompts, which made it easy to miss that scaffolding,
running a Twenty server, and live-syncing changes are three separate
concepts. When the user hits a failure (Docker not running, server not
up, auth not authorized), they have no mental map for which step they're
in — so they end up retrying `yarn twenty dev`, which is the only
command they remember.
## What changes
**[getting-started.mdx](https://github.com/twentyhq/twenty/blob/docs/restructure-getting-started-three-phases/packages/twenty-docs/developers/extend/apps/getting-started.mdx):**
- New summary table at the top showing the three-phase arc:
| Phase | What you do | Tool | Result |
|---|---|---|---|
| **1. Scaffold** | Generate the app's source code | `npx
create-twenty-app` | A TypeScript project on disk |
| **2. Run a server** | Start a Twenty server to sync into | Docker +
`yarn twenty server` | A running Twenty instance |
| **3. Sync** | Live-sync your code to the server | `yarn twenty dev` |
Your changes appear in the UI |
- Three top-level sections, one per phase, each ending with **"After
this phase: you have X"** so users can self-diagnose where they got
stuck.
- Phase 2 leads with the sentence that was missing before: *"Your app
needs a Twenty server to sync into. The server is a full Twenty instance
— UI, GraphQL API, PostgreSQL — running locally in Docker."* This is the
concept new users were missing.
- Removed the standalone *What are apps?* section — that's what the Core
Concepts page is for. Don't duplicate.
- Tightened wording throughout; same screenshots, same callouts, same
content depth.
**[core-concepts/apps.mdx](https://github.com/twentyhq/twenty/blob/docs/restructure-getting-started-three-phases/packages/twenty-docs/getting-started/core-concepts/apps.mdx):**
- Removed the install snippet (`npx create-twenty-app`, `cd`, `yarn
twenty dev`) — it duplicated Getting Started and the two examples used
different directory names.
- Updated the link card to reflect the new three-phase structure.
## Out of scope (mentioned for context, not done here)
- The "Docker is not running" message rewrite: separate PR
([#20280](https://github.com/twentyhq/twenty/pull/20280)).
- A `yarn twenty start` one-command bootstrap that auto-starts the
server before `dev`. Worth doing — keeping it out of this docs PR.
- Auto-offering to start the server when `yarn twenty dev` finds no
running one. Same.
- An "agent path" doc (single-page, imperative, for AI assistants) —
separate effort.
## Test plan
- [x] `npx nx lint twenty-docs` passes (no new warnings)
- [x] All `<Note>`, `<Warning>`, `<Card>`, image refs preserved
- [ ] Render and click through both pages once merged and previewed
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Summary
`catalog-sync` is a server-side admin action — it asks the connected
Twenty server to refresh its marketplace catalog from npm. It doesn't
operate on the local app code (like `build`, `deploy`, `publish`), so
having it sit at the same root level as those commands is a navigability
problem. With 13 commands at the root today, every needless one makes
the help output harder to scan.
This PR moves it under `server`:
```
# New (preferred)
yarn twenty server catalog-sync
yarn twenty server catalog-sync --remote production
# Old (still works, prints deprecation warning)
yarn twenty catalog-sync
```
Also slightly broadens the `server` group description from "Manage a
local Twenty server instance" to "Manage a Twenty server (local instance
and server-side actions)" since `catalog-sync` can target a remote.
## Help output (after)
```
$ yarn twenty --help
Commands:
...
catalog-sync [options] [Deprecated] Moved under server. Use `yarn twenty server catalog-sync`.
...
server Manage a Twenty server (local instance and server-side actions)
$ yarn twenty server --help
Commands:
start [options] Start a local Twenty server
stop [options] Stop the local Twenty server
logs [options] Stream Twenty server logs
status [options] Show Twenty server status
reset [options] Delete all data and start fresh
upgrade [options] [version] Upgrade the twenty-app-dev Docker image
catalog-sync [options] Trigger a marketplace catalog sync on the server
```
## Backwards compatibility
The top-level `yarn twenty catalog-sync` still works and runs the same
logic. It prints a yellow warning suggesting the new path, then executes
normally. Plan is to remove it in a future release.
## Test plan
- [x] `npx nx typecheck twenty-sdk` passes
- [x] `npx nx lint twenty-sdk` passes
- [x] `yarn twenty --help` shows the deprecated entry
- [x] `yarn twenty server --help` lists the new subcommand
- [x] `yarn twenty catalog-sync --help` shows the deprecation message in
the description
- [ ] End-to-end: invoking either path triggers a sync against a running
server
## Possible follow-ups
This is one slice of the bigger CLI flattening discussed offline. Other
natural moves: group `build/deploy/publish/install/uninstall/typecheck`
under an `app` group, group `add/exec/logs` under `entity`. Doing those
in their own PRs to keep blast radius small.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
# Introduction
This PR introduces a workflow and nx command that allow bumping to a
given version or incrementing the current `TWENTY_CURRENT_VERSION`
Combined with accurate on point cd triggered and CI upgrade sequence
guard mutation workflow the window where a PR can corrupt an already
released twenty version is mitigated
## Summary
Replaces the bolted-on `isTool` + `toolInputSchema` fields on
`LogicFunctionManifest` with two distinct, opt-in triggers that align
with the existing `cron` / `databaseEvent` / `httpRoute` trigger
pattern:
- **`toolTriggerSettings`** — exposes the function as an AI tool (chat /
MCP / function calling). Uses standard JSON Schema (the format LLMs
natively understand).
- **`workflowActionTriggerSettings`** — exposes the function as a step
in the visual workflow builder. Uses Twenty's rich `InputSchema` so the
builder can render proper `FieldMetadataType`-aware editors, variable
pickers, labels, and an optional `outputSchema`.
A function can opt into none, one, or both. Each surface gets the schema
format appropriate for it.
### Why
`isTool: true` previously exposed the function as both an AI tool AND a
workflow node, with the same JSON Schema feeding both — but the workflow
builder really wants Twenty's `InputSchema` (with `CURRENCY`,
`RELATION`, `EMAILS`, etc.) and the AI surface really wants standard
JSON Schema. Today the workflow builder hacks around this by treating
JSON Schema as `InputSchema`, which silently breaks for any
non-primitive field type. Splitting the triggers fixes that and lets
each surface evolve independently.
### Migration
- **Fast** instance command adds the two new nullable columns.
- **Slow** instance command backfills `toolTriggerSettings` +
`workflowActionTriggerSettings` from `isTool=true` rows (preserving
today's both-surfaces behaviour) then drops the legacy columns.
### Stacked
Stacked on top of #20181. Merge that first, then this.
## Test plan
- [ ] CI green (oxlint, typecheck, jest, vitest)
- [ ] Run `--include-slow` upgrade against a workspace with existing
`isTool=true` logic functions; verify both new columns populated and old
columns dropped
- [ ] Verify AI chat sees migrated tool functions (Linear create-issue,
Exa search) and can call them with the JSON Schema
- [ ] Add an AI-tool function from the Settings UI (toggles
`toolTriggerSettings`) and verify it shows up in chat
- [ ] Add a workflow-action function from the Settings UI (toggles
`workflowActionTriggerSettings`) and verify it appears in the workflow
node picker
- [ ] In the workflow builder, edit a `LOGIC_FUNCTION` step and verify
input fields render (no more JSON-Schema-as-InputSchema hack)
- [ ] Try defining a function with no triggers in the SDK and verify
`defineLogicFunction` rejects it
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: martmull <martmull@hotmail.fr>
## Summary
Follow-up to #20260. The `MorphRelationManyToOneFieldDisplay` component
(used for polymorphic MANY_TO_ONE relations) was missing the FK-presence
check that `RelationToOneFieldDisplay` already has.
When RLS hides a related record (e.g., a Rocket with a policy filtering
by name), the API response contains a populated FK
(`polymorphicOwnerRocketId`) but a `null` relation object. The component
was rendering an empty cell instead of the "Not shared" lock icon.
**Fix:**
- In `useMorphRelationToOneFieldDisplay`, read the record from the store
and check if any morph relation FK field is populated while the relation
value is null
- In `MorphRelationManyToOneFieldDisplay`, render
`<ForbiddenFieldDisplay />` when that condition is true
| Scenario | FK in response | Relation object | Frontend display |
|----------|---------------|-----------------|-----------------|
| Live record | "abc" | `{ id: "abc", ... }` | Record chip |
| Soft-deleted record | null | null | Empty cell |
| RLS-hidden record | "abc" | null | "Not shared" |
## Test plan
- Create a polymorphic MANY_TO_ONE relation (e.g., Pet → Rocket)
- Add an RLS policy on the target object (e.g., Rocket name contains
"Starship")
- Verify the morph relation field shows "Not shared" (lock icon) for
RLS-hidden records
- Verify live records still display normally as record chips
- Verify soft-deleted records still display as empty cells
## Summary
- Recreates the `ci-website.yaml` workflow that was removed alongside
`twenty-website` in #20270, now scoped to `twenty-website-new`.
- Replaces the old build-only job with a `[lint, typecheck, test]`
matrix run via `./.github/actions/nx-affected` on `tag:scope:website` —
same idiom used by `ci-shared.yaml`.
- Path filter watches `packages/twenty-website-new/**` and
`packages/twenty-shared/**` (since website-new depends on
`twenty-shared`), plus `package.json` / `yarn.lock`.
## Test plan
- [ ] CI Website workflow appears on this PR and the `lint`,
`typecheck`, `test` matrix jobs all pass
- [ ] `ci-website-status-check` is green
# Introduction
Support multiple selected record ids for headless front components
### Changes
**Added:**
- `recordIds: string[]` field to `FrontComponentExecutionContext`
- `useRecordIds()` hook to get all selected record IDs
**Deprecated:**
- `recordId` field - use `recordIds` instead
- `useRecordId()` hook - use `useRecordIds()` instead
Backward compatibility is preserved
## Summary
The example directory name in our scaffolding instructions was
inconsistent across docs:
| Source | Name used |
|--------|-----------|
| `create-twenty-app` README | `my-twenty-app` |
| Getting Started (developer docs) | `my-twenty-app` |
| Core Concepts → Apps (intro doc) | `my-app` ⚠️ |
| `twenty-sdk` README | `my-app` ⚠️ |
This means a user reading the high-level Apps intro sees `my-app`, then
the official Getting Started guide and the scaffold use `my-twenty-app`.
Small but eroding for confidence on the very first command.
This PR aligns the two outliers to `my-twenty-app`. The `twenty-my-app`
example in `publishing.mdx` is left alone — that's an npm package name
example, not a directory name (different concept).
## Test plan
- [x] `grep -rn "my-app\b"` over source docs returns no other
directory-name occurrences
- [ ] Verify rendered docs after merge
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
`APPEND` used display name `Sent` instead of `INBOX.Sent`
Fix is to use mailbox path, extreacted this as a utility, all services
are consistent now.
/closes #20267
## Summary
PR #20181 left `ConnectionProvider` in the `SyncableEntity` enum but
bypassing the standard sync pipeline — manifest sync called the bespoke
`ApplicationOAuthProviderService.upsertManyFromManifest()` instead of
going through the workspace-migration orchestrator like every other
SyncableEntity. Anything that assumed *"all SyncableEntity values flow
through the same pipeline"* (dev UI sync tracking, verification tooling)
was wrong about ConnectionProvider — that's the inconsistency this PR
closes.
This PR follows the `.cursor/skills/syncable-entity-*` guides
religiously, all six steps.
## What changes
**Step 1 — Types & Constants** (`@syncable-entity-types-and-constants`)
- Add `connectionProvider` to `ALL_METADATA_NAME` (twenty-shared)
- Make `ApplicationOAuthProviderEntity` extend `SyncableEntity` (drops
the ad-hoc columns since the base class provides them, adds `deletedAt`,
drops the old `(applicationId, universalIdentifier)` unique in favour of
SyncableEntity's `(workspaceId, universalIdentifier)`)
- `FlatConnectionProvider`, `FlatConnectionProviderMaps`,
`FLAT_CONNECTION_PROVIDER_EDITABLE_PROPERTIES`,
`UniversalFlatConnectionProvider`, six action types
- Register in **all** the central registries:
`AllFlatEntityTypesByMetadataName`,
`ALL_METADATA_ENTITY_BY_METADATA_NAME`,
`ALL_ENTITY_PROPERTIES_CONFIGURATION`, `ALL_MANY_TO_ONE_*`,
`ALL_ONE_TO_MANY_*`, `ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION`,
`ALL_METADATA_SERIALIZED_RELATION`,
`ALL_JSONB_PROPERTIES_WITH_SERIALIZED_RELATION`,
`WORKSPACE_CACHE_KEYS_V2` (`flatConnectionProviderMaps`),
`METADATA_EVENTS_TO_EMIT`
- `case 'connectionProvider':` in seven discriminated-union switches
(`derive-metadata-events-*`, `optimistically-apply-*`,
`enrich-create-*`)
**Step 2 — Cache & Transform** (`@syncable-entity-cache-and-transform`)
- `WorkspaceFlatConnectionProviderMapCacheService` (extends
`WorkspaceCacheProvider`, decorated with `@WorkspaceCache`,
soft-delete-aware)
- `fromConnectionProviderEntityToFlatConnectionProvider` util
- `fromConnectionProviderManifestToUniversalFlatConnectionProvider` util
- `FlatConnectionProviderModule` wires the cache service
- Wired the manifest converter into
`compute-application-manifest-all-universal-flat-entity-maps`
**Step 3 — Builder & Validation**
(`@syncable-entity-builder-and-validation`)
- `FlatConnectionProviderValidatorService` — never throws, returns error
arrays; uses indexed `byUniversalIdentifier` for the (name,
applicationUniversalIdentifier) uniqueness check (no
`Object.values().find()` on the hot path)
- `WorkspaceMigrationConnectionProviderActionsBuilderService`
- Registered in both validators-module + builder-module
- **Wired into the orchestrator** (the most-commonly-forgotten step per
the rule) — constructor inject, destructure
`flatConnectionProviderMaps`, `validateAndBuild`, append actions to the
final migration
**Step 4 — Runner & Actions** (`@syncable-entity-runner-and-actions`)
- Three handlers (create / update / delete) using the canonical
`WorkspaceMigrationRunnerActionHandler` mixin
- Registered in `WorkspaceSchemaMigrationRunnerActionHandlersModule`
**Step 5 — Integration** (`@syncable-entity-integration`)
- Delete the `upsertManyFromManifest` bypass on
`ApplicationOAuthProviderService`
- Remove the bypass call from `ApplicationSyncService` — manifest sync
now flows through the standard pipeline
- Drop `ApplicationOAuthProviderModule` from `ApplicationManifestModule`
(no longer needed)
- Import `FlatConnectionProviderModule` from
`ApplicationOAuthProviderModule` to keep the cache discoverable
- 3 new exception codes: `INVALID_CONNECTION_PROVIDER_INPUT`,
`CONNECTION_PROVIDER_NOT_FOUND`,
`CONNECTION_PROVIDER_NAME_ALREADY_EXISTS`
**Migration**
- Generated via `database:migrate:generate` (instance command
`1777896012579`): drops the old `(applicationId, universalIdentifier)`
unique constraint, adds `deletedAt` column, adds the `(workspaceId,
universalIdentifier)` unique index that `SyncableEntity` requires.
- Verified clean — a second `migrate:generate` pass produces zero drift.
**Step 6 — Tests** (`@syncable-entity-testing`)
- 3 new specs for the manifest converter (defaults, optional fields,
all-fields)
- All 32 existing OAuth-provider tests still pass
- ConnectionProvider has no end-user GraphQL CRUD (it's manifest-driven
only), so the GraphQL integration suite that other SyncableEntities ship
doesn't apply here
**Codegen**
- Regenerated GraphQL artifacts (twenty-front + twenty-client-sdk)
against the live schema
## Why this matters
Before:
- `ConnectionProvider` claimed to be a `SyncableEntity` (in the enum)
- But the entity didn't extend `SyncableEntity`
- And the manifest sync bypassed the standard pipeline
- → Verification tooling, dev UI sync tracking, anything iterating over
`ALL_METADATA_NAME` got inconsistent behaviour
After:
- `ConnectionProvider` is a `SyncableEntity` end-to-end
- Single sync path through the workspace-migration orchestrator (same as
`agent`, `skill`, `frontComponent`, `webhook`, …)
- One mental model
## Out of scope (deliberate)
- **Renaming the table** from `applicationOAuthProvider` to
`connectionProvider` — the `metadataName` is `connectionProvider` (what
consumers see in code); the table name is internal. A rename would
balloon this PR with mechanical churn unrelated to the sync-pipeline
wiring. Worth doing as a follow-up.
- **`applicationVariable` SyncableEntity conversion** — the other
manifest-sync holdout. Tracked in #20215.
## Test plan
- [ ] Migration up/down clean against fresh DB
- [ ] Install an app whose manifest declares connection providers —
providers appear in the workspace
- [ ] Re-deploy the app with one provider added, one removed, one
renamed → all reconciled correctly via the sync pipeline
- [ ] Verify the dev-UI sync-tracking page shows ConnectionProvider
entries the same way it shows agents/skills/etc
- [ ] OAuth flow still works (existing connections, new connections,
reconnect, list/get from SDK) — should be unchanged since the runtime
code path didn't move
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
Fixes#20076 (supersedes #20250)
When a related record is soft-deleted, the frontend displays "Not
shared" (lock icon) because it sees a populated FK but a null relation
object. This is misleading -- the record was deleted, not
permission-restricted.
**Backend fix** (`process-nested-relations-v2.helper.ts`):
- For MANY_TO_ONE relations, widen the relation query with
`.withDeleted()` and include `deletedAt` in the select
- In `assignRelationResults`, if the matched record has `deletedAt` set,
nullify both the FK and the relation object in the API response
- Records filtered by RLS are still not returned (even with
`withDeleted()`), so they correctly continue to show "Not shared"
- Strip `deletedAt` from relation results before returning to the client
**Frontend fix** (`RelationFromManyFieldDisplay.tsx`):
- For ONE_TO_MANY junction relations, return `null` instead of
`<ForbiddenFieldDisplay />` when junction records exist but target
records are unavailable
### Three cases now handled correctly:
| Scenario | FK in response | Relation object | Frontend display |
|---|---|---|---|
| **Live record** | `"abc"` | `{ id: "abc", ... }` | Record chip |
| **Soft-deleted record** | `null` | `null` | Empty cell |
| **RLS-hidden record** | `"abc"` | `null` | "Not shared" |
## Test plan
- [ ] Create a record with a MANY_TO_ONE relation (e.g., a person linked
to a company)
- [ ] Soft-delete the related record (the company)
- [ ] Verify the relation field shows an empty cell, not "Not shared"
- [ ] Restore the related record and verify the relation reappears
- [ ] Verify that RLS-hidden relations still show "Not shared"
Made with [Cursor](https://cursor.com)
Co-authored-by: Cursor <cursoragent@cursor.com>
## Summary
- Adds `UpgradeGaugeService` that exposes three observable Prometheus
gauges based on the recently merged upgrade status service:
- `twenty_upgrade_instance_health` — 1 (up-to-date), 0 (behind), -1
(failed)
- `twenty_upgrade_workspaces_behind_total` — count of workspaces with
pending upgrade commands
- `twenty_upgrade_workspaces_failed_total` — count of workspaces with a
failed upgrade command
- Follows the existing gauge pattern (`WorkspaceGaugeService`,
`BillingGaugeService`, `DatabaseGaugeService`)
### Caching & QPS design
Prometheus scrapes every **15s** via `ServiceMonitor`. Each gauge uses
the `MetricsService.createObservableGauge({ cacheValue: true })` pattern
which caches the value in Redis for **60 seconds**. Under that,
`UpgradeStatusService.getInstanceAndAllWorkspacesStatus()` uses
`UpgradeStatusCacheService` with a **1-hour TTL** in Redis.
Result: at most 1 DB query per hour regardless of scrape frequency.
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
## Summary
The Notes widget (and other record-bound widgets like Tasks, Files,
Calendar, Emails) incorrectly displayed "Invalid Configuration" when
rendered on dashboard or standalone page contexts. The root cause was an
overly broad `ErrorBoundary` that caught all runtime errors uniformly
and displayed a misleading error message.
## Related issue
Fixes: #20118
## Problem Analysis
**Proximate Cause:**
- `NotesCard` calls `useTargetRecord()` which throws a generic `Error`
when `targetRecordIdentifier` is undefined
- `ErrorBoundary` in `WidgetCardShell.tsx` catches this error and
renders `PageLayoutWidgetInvalidConfigDisplay`
- This displays "Invalid Configuration" which is factually misleading
**Triggering Cause:**
- Commit 5cd8b7899d removed the feature flag gate on page layouts,
making them standard for all workspaces
- This exposed record-bound widgets to dashboard contexts where
`targetRecordIdentifier` is intentionally undefined
**Error Propagation Chain:**
```
WidgetContentRenderer → NoteWidget → NotesCard → useTargetRecord()
useTargetRecord() throws Error('useTargetRecord must be used within a record page context')
ErrorBoundary catches error → PageLayoutWidgetInvalidConfigDisplay renders misleading UI
```
## Solution
Introduced a distinction between **configuration errors** and **record
context requirement errors** by:
1. Creating a custom error class `RecordContextRequiredError`
2. Updating `useTargetRecord()` to throw this specific error type
3. Creating a dedicated display component for record context errors
4. Updating the `ErrorBoundary` fallback to handle error types
appropriately
## User Impact
| Before | After |
|--------|-------|
| "Invalid Configuration" (red badge) | "Record Required" (gray badge) |
| Misleading error message | Accurate context-aware message |
| Users think widget is broken | Users understand widget needs record
context |
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
## Summary
- Resolve standard field `label`, `description`, and `icon` overrides
through dedicated GraphQL field resolvers.
- Fall back to the source locale safely when the request locale is
missing, and allow direct overrides to apply for non-source locales when
translations are absent.
- Enrich metadata subscription payloads for both `before` and `after`,
reusing the same override application path for field and object
metadata.
- Update and extend tests to cover the revised override behavior.
## Testing
- Updated unit coverage for standard override resolution, including the
non-source-locale fallback path.
- Not run (not requested).
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Fixes#20193
**Bug Description:**
Previously, workspace member avatars failed to render correctly in table
views and relation chips (such as the Account Owner field). While the
avatar picker dropdown correctly fetched fresh GraphQL data, table views
and chips relied on the cached defaultAvatarUrl or avatarUrl fields,
which were frequently resolving to empty strings or failing to parse
external OAuth URLs correctly.
**Root Cause:**
- Empty String Defaults: Deleting an avatar or failing to retrieve one
defaulted the database state to an empty string ("") instead of null,
which caused frontend image components to break rather than render their
fallback states.
- Missing Permanent URLs: The WorkspaceMemberTranspiler was strictly
expecting internal signed URLs. If an avatar was an external OAuth URL,
it incorrectly returned an empty string, breaking SSO profile pictures.
- Missing Fallbacks: New users lacked a proper Gravatar fallback
assignment upon workspace creation.
**Changes Made:**
- user-workspace.service.ts: Updated the avatar computation logic during
user creation to implement a reliable Gravatar fallback and correctly
set missing avatars to null instead of empty strings. Updated the
storage to use permanent file URLs.
- file-url.service.ts: Implemented a getRawFileUrl method to support
rendering permanent, non-expiring file URLs for avatars.
- workspace-member-transpiler.service.ts: Refactored the URL
transpilation logic to gracefully pass through external OAuth URLs
(e.g., Google/Microsoft profile pictures) instead of stripping them.
- WorkspaceMemberPictureUploader.tsx: Fixed the frontend removal logic
so that deleting a profile picture sets the avatarUrl to null
(consistent with the backend) rather than an empty string.
**Testing:**
- Verified that avatars correctly display in relation chips and table
views.
- Verified that external OAuth avatars load properly.
- Verified that deleting an avatar correctly resets the UI to the
fallback initials component.
Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Fixes#20128
## Summary
Fix REST API filter parsing when bare filters are mixed with explicit
conjunctions.
## What changed
- Replaced the loose parentheses check in
`addDefaultConjunctionIfMissing` with proper root conjunction detection.
- Shared the root conjunction regex with `parseFilter`.
- Added regression tests for mixed filters like
`status[eq]:'TODO',and(title[ilike]:'%test%')`.
## Validation
- `npx nx test twenty-server
--testPathPatterns=add-default-conjunction.util.spec.ts --runInBand
--coverage=false`
- `npx prettier --check ...`
## Summary
Replaces the hardcoded `0` in
`ViewBarFilterDropdownAdvancedFilterButton` with the actual count of
active advanced filter rules, matching the behavior of
`AdvancedFilterChip` in the view bar.
## What changed
In
`packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownAdvancedFilterButton.tsx`:
- Imported `useAtomComponentSelectorValue` and
`rootLevelRecordFilterGroupComponentSelector`
- Imported `useChildRecordFiltersAndRecordFilterGroups`
- Replaced `const advancedFilterQuerySubFilterCount = 0; // TODO` with
the real computed count via the same hook pattern used in
`AdvancedFilterChip.tsx`
The pill badge will now appear on the "Advanced filter" dropdown menu
item showing the number of active advanced filter rules (e.g. "2" when
two rules are active).
## References
- Fixes#20207
---------
Co-authored-by: wadeKeith <wade@twenty.app>
Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
## Summary
- When an event stream expires (TTL), `addQueryToEventStream` and
`removeQueryFromEventStream` now return `false` instead of throwing
`EVENT_STREAM_DOES_NOT_EXIST` as an `InternalServerError`
- Frontend checks the mutation return value and triggers the
destroy/recreate cycle, same recovery behavior without the error path
- Removes `EVENT_STREAM_DOES_NOT_EXIST` from exception code, exception
filter, and frontend graceful error check since it's no longer thrown
## Test plan
- [x] Verify that when an event stream TTL expires, the frontend
silently recreates the stream without error noise in logs/Sentry
- [x] Verify that `NOT_AUTHORIZED` errors still throw correctly on both
mutations
- [ ] Verify that the subscription `onEventSubscription` still works
end-to-end with stream creation, query registration, and heartbeat TTL
refresh
Made with [Cursor](https://cursor.com)
## Summary
Adds an admin upgrade-status panel that surfaces per-instance and
per-workspace migration health, backed by a Redis-cached aggregate to
keep the page snappy on large fleets.
<img width="827" height="880" alt="Screenshot 2026-04-28 at 10 21 03"
src="https://github.com/user-attachments/assets/8f88baa9-7268-4eff-bf6a-906a7f06ca91"
/>
<img width="804" height="892" alt="Screenshot 2026-04-28 at 10 21 11"
src="https://github.com/user-attachments/assets/1e6decf8-766a-4d0e-96b1-03a9962bba3c"
/>
## Computed metrics
**Instance** (`InstanceUpgradeStatus`)
- `inferredVersion` — version derived from the latest non-initial
instance command name
- `health` — `upToDate` | `behind` | `failed`, derived from the latest
attempt vs. the last expected instance step in the upgrade sequence
- `latestCommand` — `{ name, status, executedByVersion, errorMessage,
createdAt }` from the most recent attempt
**Per-workspace** (`WorkspaceUpgradeStatus`)
- `workspaceId`, `displayName`
- `inferredVersion`, `health`, `latestCommand` (same shape as instance),
computed against the latest expected step in the sequence
**Aggregate** (`AllWorkspacesUpgradeStatus`, only across `ACTIVE` /
`SUSPENDED` workspaces)
- `instanceUpgradeStatus`
- `totalCount`, `upToDateCount`, `behindCount`, `failedCount`
- `workspacesBehindIds[]`, `workspacesFailedIds[]`
- `computedAt`
## Fetching strategy
All reads go through `UpgradeStatusCacheService` (cache namespace:
`EngineHealth`).
- **Aggregate read** (`getAllWorkspacesStatus` →
`getAllWorkspacesUpgradeStatus` query):
reads summary + behind-ids + failed-ids in parallel; if any of the three
keys is missing, full recompute (`recomputeAllWorkspaces`) is triggered,
which also primes per-workspace entries.
- **Per-workspace read** (`getWorkspacesStatus(ids)` →
`getUpgradeStatus(ids)` query):
`mget` on workspace keys; misses are recomputed individually
(`recomputeWorkspace`), and aggregates are reconciled in place (count +
id list deltas) without a full recompute.
- **Recompute on demand**: `refreshUpgradeStatus` mutation calls
`recomputeAllWorkspaces` to bypass cache and rewrite all keys.
- **Auto-invalidation**: `InstanceCommandRunnerService` (fast + slow
paths) and `WorkspaceCommandRunnerService` invalidate after every run
via `safeInvalidateUpgradeStatusCache()`
(`flushByPattern('upgrade-status:*')`). Failures in cache invalidation
are swallowed and logged so they never break the migration runner.
- **TTL**: `60 * 60 * 1000` ms (1 hour) on every key — protects against
stale data even if a runner crashes before invalidating.
## Introduced cache keys
All under the `EngineHealth` cache-storage namespace:
| Key | Type | Purpose |
| --- | --- | --- |
| `upgrade-status:all-workspaces:summary` |
`CachedAllWorkspacesStatusSummary` | Counts + instance status +
`computedAt` |
| `upgrade-status:all-workspaces:behind-ids` | `string[]` | Workspace
ids in `behind` state |
| `upgrade-status:all-workspaces:failed-ids` | `string[]` | Workspace
ids in `failed` state |
| `upgrade-status:workspace:<workspaceId>` |
`CachedWorkspaceUpgradeStatus` | Per-workspace status (one key per
workspace) |
Full invalidation uses the pattern `upgrade-status:*`.
## Index added on `upgradeMigration` (already added on prod)
Migration
`2-2-instance-command-fast-1777308014234-addUpgradeMigrationWorkspaceIdIndex.ts`:
```sql
CREATE INDEX "IDX_upgradeMigration_workspaceId_name_attempt"
ON "core"."upgradeMigration" ("workspaceId", "name", "attempt")
WHERE "workspaceId" IS NOT NULL;
## Problem
When a `workspaceMember` is updated (e.g., theme/locale/avatar changes),
the `WorkspaceMemberAvatarFileDeletionListener` triggers file deletion.
If the referenced file entity doesn't exist in the database, an
unhandled `EntityNotFoundError` crashes the NestJS server, causing a 502
loop.
## Change
Wrap the file deletion call in a try-catch that gracefully handles
`EntityNotFoundError` as a no-op — if the file doesn't exist, there's
nothing to delete.
Fixes#20191.
Made with [Cursor](https://cursor.com)
Co-authored-by: martmull <martmull@hotmail.fr>
# Introduction
Aiming for faster cd process
## Splitting front end server deps
Reduce dependencies bloating when target is server only, installing only
root repo dev deps and server dev and prod deps
Still pruning before copying to prod node_modules
## Server only remove twenty-ui
Also removing twenty-ui from server build as it was not consumed at all
Depends on https://github.com/twentyhq/twenty/pull/20140