Commit Graph
4435 Commits
Author SHA1 Message Date
f720186122 chore: bump version to 2.4.0 (#20345)
## Summary

- Moves current version to previous versions array
- Sets TWENTY_CURRENT_VERSION to the new version
- Updates TWENTY_NEXT_VERSIONS with the next minor version

## Checklist

- [x] Verify version constants are correct

---------

Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com>
Co-authored-by: prastoin <paul@twenty.com>
2026-05-07 09:41:47 +00:00
martmullandGitHub 900f70fb9e Add description to oAuth_only app created (#20336)
## Before

<img width="1000" height="555" alt="image"
src="https://github.com/user-attachments/assets/bda317c7-93e7-448e-8e65-a9cc1be6df95"
/>

## After
<img width="1010" height="504" alt="image"
src="https://github.com/user-attachments/assets/5cbc5c6c-5cc3-40d8-9545-ba0f3d2675b4"
/>
<img width="980" height="574" alt="image"
src="https://github.com/user-attachments/assets/4386fe56-0614-4a4e-82a0-c9f46af69c85"
/>
2026-05-07 09:02:04 +00:00
EtienneandGitHub 81f351c90c Ai provider - fix (#20318) 2026-05-07 10:02:54 +02:00
martmullandGitHub 948ed964bb Add isConfigured to application registration in App admin panel (#20326)
## After
Added "Configured" column in admin panel apps tab
<img width="1171" height="611" alt="image"
src="https://github.com/user-attachments/assets/e6850b39-5789-4ad2-b3df-192a28cb03e2"
/>

Add a banner to ask to configure the application 
<img width="1218" height="483" alt="image"
src="https://github.com/user-attachments/assets/1491363c-3479-41ca-97b7-c7dc9e07ff16"
/>
2026-05-07 09:59:01 +02:00
eddd2c979a Add Workspace Created and Payment Received ClickHouse events (#20277)
## Summary
- Adds two new workspace audit events for the AARRR funnel tracked in
ClickHouse
- **Workspace Created**: emitted in `signUpOnNewWorkspace` after the
transaction commits, capturing every new workspace creation
- **Payment Received**: emitted in `processInvoicePaid` on every Stripe
`invoice.paid` webhook, with `stripeInvoiceId`, `amountPaid`, and
`billingReason` properties. First payment per workspace can be derived
at query time via `min(timestamp)` grouped by `workspaceId`

## Test plan
- [x] Verify `Workspace Created` event appears in ClickHouse after
signing up on a new workspace
- [x] Verify `Payment Received` event appears in ClickHouse after a
Stripe `invoice.paid` webhook fires
- [x] Confirm no event is emitted if the billing customer cannot be
resolved from `stripeCustomerId`
- [x] Run existing `SignInUpService` unit tests pass with the new
`AuditService` mock


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-07 09:57:30 +02:00
Charles BochetandGitHub 10876138d2 refactor: stop reading joinColumnName from relation field settings (#20304)
## Summary

`joinColumnName` on relation field settings is always derivable from the
field name (and the target object name for morph relations). This PR
stops reading it from settings anywhere in production code; the stored
value is no longer used.

The settings field is **not** removed from data yet — a follow-up can
drop it once we are confident nothing depends on the stored value.

## Helpers

The helpers are split by layer because frontend and backend hold morph
relations differently: the frontend has a base name plus a
`morphRelations[]` array, the backend has one row per target with the
name already morph-resolved.

| Helper | Layer | When to use |
|---|---|---|
| `computeRelationGqlFieldJoinColumnName` | Shared / frontend
(`gqlField`) | Non-morph relation on the frontend. |
| `computeMorphRelationGqlFieldName` | Shared / frontend (`gqlField`) |
Need the per-target morph gqlField name (e.g. `targetCompany`). |
| `computeMorphRelationGqlFieldJoinColumnName` | Shared / frontend
(`gqlField`) | Per-target morph join column on the frontend. Prefer over
the non-morph helper for any morph field — it forces the per-target
inputs. |
| `computeMorphOrRelationFieldJoinColumnName` | Backend
(`FlatFieldMetadata.name`) | Any backend read or write — the flat name
is already morph-resolved, so one helper covers both cases. |
| `computeMorphRelationFlatFieldName` | Backend
(`FlatFieldMetadata.name`) | **Mutation paths only** (create / update /
object rename). Reads consume the stored `field.name` and never call
this. |

## Test plan

- [x] Typecheck and lint (front, server, shared)
- [x] Existing unit tests pass
- [ ] CI green
2026-05-07 09:53:00 +02:00
Charles BochetandGitHub 83c40bb8cc fix(server): bypass workspace cache in onboardingStatus resolver (#20322)
## Summary

In multi-instance deployments, `coreEntityCacheService` memoizes the
workspace entity per server for ~10s, bypassing Redis hash invalidation.
After `activateWorkspace`, if the next `currentUser` query is routed to
a stale replica, the server returns `onboardingStatus:
WORKSPACE_ACTIVATION` and `workspaceMember: null`, the client redirects
to `/create/profile`, and submitting the form throws "User is not logged
in". Reproduces on prod/staging only (local dev = single instance).

Fix: in `OnboardingService.getOnboardingStatus({ user, workspaceId })`,
read the workspace directly from `WorkspaceEntity` repository (bypassing
the per-instance core entity cache) so `onboardingStatus` reflects the
freshest `activationStatus` right after `activateWorkspace`, even when
the request hits a replica with a stale cached workspace.

## Test plan

- Prod/staging: sign up + create workspace, verify `/create/profile`
works and form submits.
- Local: regression on the full onboarding flow.
2026-05-06 17:41:46 +02:00
martmullandGitHub 2158266fcc Fix migration (#20321)
as title
2026-05-06 15:05:13 +02:00
Paul RastoinandGitHub 26874c3603 Nest command unhandled error process exit 1 (#20312)
# Introduction
When running the `run-instance-commands` on a migration failure the
process wouldn't throw at all
Leading to conditional flow to keep going whereas it should have stopped
This update is very invasive and impacts all the nest commander
registered commands
We should keep in mind that it impacts the way we create and init
database and so on

But I think that's for the best, as cli that never exit 1 is
counterintuitive
2026-05-06 09:26:42 +00:00
88988e5a55 i18n - translations (#20313)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-06 10:42:40 +02:00
martmullandGitHub 617f571400 20215 convert application variable to a syncable entity (#20269)
##  Summary

- Converts applicationVariable from a bespoke sync path to a proper
SyncableEntity,
unifying it with the workspace migration pipeline used by all other
manifest-managed
  entities (agent, skill, frontComponent, webhook, etc.)
- Removes the upsertManyApplicationVariableEntities method and its
direct-DB-mutation
approach in favor of the standard validate → build → run action handler
pipeline
- Adds universalIdentifier, deletedAt columns and makes applicationId
NOT NULL via an
  instance command migration

##  Motivation

Before this change, applicationVariable was the only manifest-managed
entity that bypassed
ApplicationManifestMigrationService.syncMetadataFromManifest(). It used
a bespoke service
method called directly from syncApplication(), creating two mental
models, two validation
styles, and two cache invalidation patterns. Now there's one unified
pipeline for all
  manifest entities.

##  What changed

###  Entity refactor:
- ApplicationVariableEntity now extends SyncableEntity (gains
universalIdentifier,
  non-nullable applicationId with CASCADE, soft-delete via deletedAt)

###  New flat entity layer (flat-application-variable/):
- Type, maps type, editable properties constant, entity-to-flat
converter, cache service,
  module

###  New migration pipeline wiring:
- Manifest converter
(fromApplicationVariableManifestToUniversalFlatApplicationVariable)
  - Validator service (FlatApplicationVariableValidatorService)
- Builder service
(WorkspaceMigrationApplicationVariableActionsBuilderService)
  - Create/Update/Delete action handlers with secret encryption hooks
- Registered in orchestrator, builder module, runner module, and all
type registries

###  Removed bespoke path:
- Deleted upsertManyApplicationVariableEntities from
ApplicationVariableEntityService
  - Removed its call from ApplicationSyncService.syncApplication()
- Kept update() (operator-set value at runtime) and getDisplayValue()
(runtime display)

###  Database migration:
- Instance command to add columns, backfill universalIdentifier, enforce
NOT NULL
  constraints, and update indexes

##  Test plan

  - npx nx typecheck twenty-server passes (0 errors)
- Unit tests pass (application-variable.service.spec.ts,
build-env-var.spec.ts)
- Install an app with applicationVariables in its manifest → variables
appear with correct
  universalIdentifier
- Update app manifest (add/remove/modify a variable) → migration
pipeline handles diff
  correctly
- Operator-set value via update endpoint persists correctly with
encryption
  - Uninstall app → variables cascade-deleted
  - app dev --once on example app syncs without errors
2026-05-06 08:23:53 +00:00
2a97e77303 fix(server): handle Redis idle disconnects in session-store client (#20143)
## Summary

The session-store node-redis client doesn't attach an `'error'` event
listener, so when Redis closes an idle connection (server-side `timeout`
setting), node-redis emits an unhandled `'error'` event and the entire
Node process crashes with `SocketClosedUnexpectedlyError`.

## Reproduction

1. Deploy twenty-server against a Redis instance with `timeout 300` (5
min idle close).
2. Don't log in (or otherwise keep the session store completely idle).
3. ~5 minutes after `Nest application successfully started`, the process
crashes:

```
node:events:487
      throw er; // Unhandled 'error' event
      ^

SocketClosedUnexpectedlyError: Socket closed unexpectedly
    at Socket.<anonymous> (/app/node_modules/@redis/client/dist/lib/client/socket.js:194:118)
    ...
Emitted 'error' event on Commander instance at:
    at RedisSocket._RedisSocket_onSocketError (/app/node_modules/@redis/client/dist/lib/client/socket.js:218:10)
```

Kubernetes restarts the pod and the loop repeats every ~5 minutes (12
restarts in 95 min in our environment).

`twenty-worker` is unaffected — BullMQ's ioredis client has its own
keep-alive and the queue keeps it busy.

## Root cause


`packages/twenty-server/src/engine/core-modules/session-storage/session-storage.module-factory.ts`
constructs the node-redis client with no error listener:

```ts
const redisClient = createClient({ url: connectionString });

redisClient.connect().catch((err) => {
  throw new Error(`Redis connection failed: ${err}`);
});
```

In Node.js, an unhandled `'error'` event on an `EventEmitter` becomes an
uncaught exception. node-redis emits `'error'` on socket close. With no
listener, the process exits 1 — even though node-redis would otherwise
reconnect on its own.

## Fix

1. Attach a `client.on('error', ...)` listener so disconnect errors are
logged. node-redis' built-in `reconnectStrategy` then takes over.
2. Set `pingInterval: 60_000` so the connection is never idle long
enough to be reaped by any reasonable Redis `timeout`. Defense in depth.

## Verification

Reproduced locally with Redis `CONFIG SET timeout 30` (30s for fast
reproduction). Without the fix: process exits 30s after boot. With the
fix: client logs the disconnect, reconnects, and the process keeps
running.

## Notes / out of scope

- `cache-storage.module-factory.ts` uses `cache-manager-redis-yet`
(which wraps node-redis under the hood). It may exhibit the same
vulnerability under sufficiently idle conditions; recommend a follow-up
to confirm and similarly harden it.
- `redis-client.service.ts` uses ioredis, which has built-in keepalive
and reconnect — no immediate crash risk, but adding error logging there
would be a nice consistency win.

## Test plan

- [ ] Existing tests still pass
- [ ] Manual: deploy with low Redis `timeout` (e.g. `30`), confirm
process survives
- [ ] Manual: kill Redis briefly, confirm twenty-server reconnects
instead of exiting

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-05 22:09:13 +00:00
bbd9720ab3 [Dashboards] [Warning] Remove gauge chart support and delete existing widgets (#20172)
## Summary

Removes gauge chart from the chart-type picker and deletes existing
gauge widgets via a workspace migration. The gauge was rendering a
hardcoded `0.7 / "Progress"` stub regardless of configuration -- never
wired to real data.

The contract stays in place. We keep
`WidgetConfigurationType.GAUGE_CHART`, the DTO, the GraphQL union
member, and the gauge folder -- so stored gauge JSON still resolves
through the schema. The render path falls through to `default: return
null`, so any un-migrated gauge widget renders as an empty cell, not a
crash.

This PR just removes existing gauge widgets if there are any (via
`upgrade:2-3:delete-gauge-widgets`). The deliberate cleanup -- deleting
the type definitions, the gauge folder, the DTO -- comes in a follow-up
PR after the migration has run.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-05 21:36:45 +00:00
a3c026f1ce i18n - translations (#20302)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-05 20:39:23 +02:00
e0563377b5 Fix unclear metadata validation errors (#20234)
https://github.com/user-attachments/assets/8f8f1122-3de1-4a9b-8bb4-a3c8d31e47ae

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-05 18:18:05 +00:00
31674253a1 i18n - translations (#20289)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-05 16:33:11 +02:00
633553f729 feat(sdk): add defineCommandMenuItem (#20256)
## 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>
2026-05-05 14:16:04 +00:00
neo773andGitHub 4dd08097ce CalDAV refactor (#20180)
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 |
2026-05-05 14:15:05 +00:00
65ba36d475 i18n - translations (#20286)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-05 15:31:52 +02:00
neo773andGitHub d040756fcf remove direction from messages (#20026)
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
2026-05-05 15:24:25 +02:00
Paul RastoinandGitHub 88394c25ef Bump twenty current version (#20241)
# 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
2026-05-05 13:05:11 +00:00
53fdac1417 feat(apps): split AI tool and workflow action triggers in LogicFunction manifest (#20208)
## 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>
2026-05-05 14:56:09 +02:00
neo773andGitHub 8d001eb33f fix: don't mark IMAP channel as failed on transient server errors (#20273)
Map RFC 5530 codes to `TEMPORARY_ERROR` so sync retries instead of
terminally flagging `FAILED_INSUFFICIENT_PERMISSIONS` when the server is
briefly unavailable.

prod Logs

```
	2026-05-05 03:53:42.129	
    authenticationFailed: true
	2026-05-05 03:53:42.129	
    serverResponseCode: 'UNAVAILABLE',
	2026-05-05 03:53:42.129	
    responseText: 'Account is temporarily unavailable.',
	2026-05-05 03:53:42.129	
	2026-05-05 03:53:42.129	
    response: '2 NO [UNAVAILABLE] Account is temporarily unavailable.',
	2026-05-05 03:53:42.129	
  cause: Error: Command failed
	2026-05-05 03:53:42.129	
  code: 'INSUFFICIENT_PERMISSIONS',
Caused by: Error: Command failed

[Nest] 35  - 05/04/2026, 10:23:42 PM   ERROR [ImapGetAllFoldersService] MessageImportDriverException: IMAP authentication error: Command failed
```
2026-05-05 14:30:57 +02:00
neo773andGitHub a3f2fafce6 fix smtp outbound persist message (#20276)
`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
2026-05-05 14:28:03 +02:00
dd3b6f2a2f i18n - translations (#20278)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-05 14:11:21 +02:00
e3be1f4971 Make ConnectionProvider a true SyncableEntity (#20232)
## 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>
2026-05-05 14:04:15 +02:00
7c4302d02a fix: show empty cell instead of 'Not shared' for soft-deleted related records (#20260)
## 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>
2026-05-05 09:32:16 +00:00
fda2295beb feat: expose upgrade status as Prometheus gauge metrics (#20262)
## 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>
2026-05-05 07:56:13 +00:00
01b4754e62 New name not appearing when renaming "Stages" in data-model settings (#20246)
## 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>
2026-05-04 20:16:00 +00:00
e6399b180e Fix/workspace member avatars 20193 (#20200)
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>
2026-05-04 19:42:12 +00:00
Sri Hari Haran SharmaandGitHub 281eaa3721 fix rest filter default conjunction detection (#20133)
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 ...`
2026-05-04 16:18:56 +00:00
Thomas TrompetteandGitHub 95a34d8517 Return false instead of throwing when event stream does not exist (#20165)
## 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)
2026-05-04 15:06:18 +00:00
MarieandGitHub 4852ac401a Add server upgrade status on admin panel (#20107)
## 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;
2026-05-04 15:06:07 +00:00
1cd983a330 fix: handle missing file entity in avatar deletion listener (#20192)
## 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>
2026-05-04 14:58:56 +00:00
91124a3cb8 AI - Add azure foundry provider (#20170)
[Merge this before](https://github.com/twentyhq/twenty-infra/pull/655)

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-05-04 14:45:06 +00:00
9ddf9af4c4 i18n - translations (#20236)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-04 16:18:30 +02:00
3ffda0a29e Add twenty version validation (#20227)
as title, server version is checked before app deploy, and app install
commands

### New section in publishing doc
<img width="1344" height="912" alt="image"
src="https://github.com/user-attachments/assets/2a9335e7-0a7a-4973-a2db-f30f03181001"
/>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-04 13:55:31 +00:00
3c7c62c79f fix(server): deduplicate @opentelemetry/api to fix NoopMeterProvider (#20231)
## Summary

**All OTel metrics in twenty-server have been silently dropped since
April 30.**

### Root cause

PR #20149 (`bump @sentry/profiling-node 10.27→10.51`) pulled in
`@sentry/node@10.51.0`, which declares `@opentelemetry/api: ^1.9.1` as a
**dependency** (not peer). Yarn installed it as a **nested** copy at
`1.9.1`, while the hoisted copy stayed at `1.9.0`.

At startup in `instrument.ts`:
1. `Sentry.init()` uses the **nested `1.9.1`** to register `trace`,
`propagation`, `context` on the OTel global → global version becomes
**`1.9.1`**
2. `setGlobalMeterProvider()` uses the **hoisted `1.9.0`** →
`registerGlobal` sees version mismatch (`1.9.1` ≠ `1.9.0`) → **silently
returns `false`**
3. Global stays `NoopMeterProvider` → every counter, gauge, and
histogram in the server is a no-op

### What this PR does

1. **Reverts three troubleshooting PRs** that are no longer needed now
that the root cause is identified:
   - #20230 — heartbeat gauge
   - #20228 — OTLP export lifecycle logs
- #20221 — Sentry revert to 10.27 (which never actually downgraded in
`yarn.lock` since `^10.27.0` resolved to `10.51.0`)

2. **Fixes the root cause**:
- Root Yarn resolution pinning `@opentelemetry/api` to `1.9.1` → single
copy in the entire tree, Sentry and Twenty share the same instance
- Named import in `instrument.ts` (`import { metrics as otelMetrics }`
instead of default import) as defense-in-depth against CJS interop
issues

### Verified on dev cluster

Exec'd into the running pod and confirmed:
- `@sentry/node` nests `@opentelemetry/api@1.9.1`, hoisted is `1.9.0`
- `Sentry.init()` → global version `1.9.1` → `setGlobalMeterProvider`
with VERSION `1.9.0` → returns `false` → `NoopMeterProvider`
- Same-version registration returns `true` → `MeterProvider` ✓

## Test plan
- [ ] CI passes (lint, typecheck, build)
- [ ] Deploy to dev cluster and verify metrics flow to collector
- [ ] Confirm `node_modules/@opentelemetry/api/package.json` shows
`1.9.1` with no nested copy under `@sentry/`

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 15:15:00 +02:00
a3cf565ba3 feat(server): add heartbeat gauge + first-export-attempt log for OTLP (#20230)
## Summary
- Adds a trivial always-on observable gauge (`twenty.heartbeat = 1`) so
the OTLP exporter fires on every 10s collection tick, even on idle pods.
Without this, `PeriodicExportingMetricReader` skips the export when
`scopeMetrics` is empty, so the process-log wrapper never runs and we
can't prove OTLP connectivity.
- Adds a one-time "first periodic export attempt" log line inside the
wrapped exporter, completing the startup log sequence: `OTLP reader
enabled` → `first periodic export attempt` → `first export ok` / `export
failed`.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 12:30:59 +02:00
Abdul RahmanandGitHub fa8304d323 Fix record table dashboard save (#20202)
## Summary

Fixes `METADATA_VALIDATION_FAILED` / “view field already exists” when
saving dashboard **record table** widgets after the first persist (e.g.
several table widgets on one dashboard).

## Problem

Draft view columns used **client-generated** `viewField` ids. After
save, the API stored **different** ids. The next upsert still sent the
old draft ids as `viewFieldId`. The server only matched on that id,
missed every row, and tried to **create** columns that already existed
for the same `fieldMetadataId` + view.
2026-05-04 09:57:21 +00:00
467ddaa27a feat(server): OTLP metrics export logs for troubleshooting (#20228)
## Summary

Adds **grep-friendly** `console` logging around the OpenTelemetry
metrics OTLP exporter in
[`packages/twenty-server/src/instrument.ts`](packages/twenty-server/src/instrument.ts)
so production / staging can confirm whether the app is exporting metrics
and why exports fail.

## Log format

- Prefix: **`[Twenty OTEL metrics]`** (easy to filter in Loki / `kubectl
logs | grep`).
- **Startup:** whether the OTLP reader is enabled, `exportIntervalMs`,
and endpoint as `protocol//host/path` only (no credentials).
- **First successful export:** one `console.log` per process (`first
export ok`) with metric data point count — avoids spamming every 10s.
- **Each failed export:** `console.warn` with result code, point count,
and serialized error (including nested `AggregateError` causes when
present).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 11:44:51 +02:00
fc4cf7fe09 i18n - translations (#20226)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-04 11:34:51 +02:00
9e94045fa5 feat(apps): generic OAuth provider support for app SDK (#20181)
## Summary

App developers can now declare third-party OAuth integrations (GitHub,
Linear, Slack, etc.) in their manifest and the platform handles the full
authorize → callback → token-exchange → refresh → injection lifecycle.
The dev writes ~10 lines of config and reads tokens via
`useOAuth('linear')` inside any logic function.

```ts
// app/src/oauth-providers/linear.ts
export default defineOAuthProvider({
  universalIdentifier: '...',
  name: 'linear',
  displayName: 'Linear',
  authorizationEndpoint: 'https://linear.app/oauth/authorize',
  tokenEndpoint: 'https://api.linear.app/oauth/token',
  scopes: ['read', 'write'],
  connectionMode: 'per-user',
  clientIdVariable: 'LINEAR_CLIENT_ID',
  clientSecretVariable: 'LINEAR_CLIENT_SECRET',
  tokenRequestContentType: 'form-urlencoded',
});

// app/src/logic-functions/handlers/...
const { accessToken } = useOAuth('linear'); // throws OAuthNotConnectedError if missing
```

## Architecture

- **Storage**: extends the existing `connectedAccount` table — new
nullable `applicationOAuthProviderId` FK + new `app` value on the
`ConnectedAccountProvider` enum. Existing Google/Microsoft flows are
untouched.
- **OAuth flow**: a single `/apps/oauth/authorize` +
`/apps/oauth/callback` controller pair handles every app provider. State
travels in a JWT signed via the existing `JwtWrapperService` (new
`APP_OAUTH_STATE` token type).
- **Token exchange**: goes through
`SecureHttpClientService.createSsrfSafeFetch()` (so an installed app
can't point `tokenEndpoint` at internal hosts).
- **Refresh**: piggybacks on the existing
`ConnectedAccountRefreshTokensService` dispatch — Google/Microsoft
drivers untouched, new app driver lives engine-side under
`application-oauth-provider/refresh/`.
- **Injection**: the executor injects refreshed tokens as env vars
(`OAUTH_<NAME>_ACCESS_TOKEN`, `_HANDLE`, `_SCOPES`, `_CONNECTED`); the
SDK helpers `useOAuth` / `useOptionalOAuth` read them.
- **Frontend**: auto-rendered "OAuth Connections" section under each
app's settings tab (no custom front component needed). App-managed
connections are filtered out of `/settings/accounts` so the
email/calendar page stays focused.
- **Disconnect**: best-effort revoke against the manifest's
`revokeEndpoint` before deleting the row.

## Reference app

`packages/twenty-apps/internal/twenty-linear/` exercises the full
pipeline:

- `defineOAuthProvider` for Linear
- `POST /linear/create-issue` and `GET /linear/teams` HTTP-route logic
functions
- Vitest tests for the handlers

## Tests

- 14 server-side Jest tests: token-exchange util (form-urlencoded vs
JSON, PKCE, error paths), flow service (authorize URL shape, state
binding, ConnectedAccount upsert on first/reconnect, per-workspace mode,
invalid state)
- 8 app-level Vitest tests: handler error paths, GraphQL request shape,
Linear error propagation
- All 4 packages clean: `npx nx lint:diff-with-main` and `npx tsc
--noEmit`

## Test plan

- [ ] Apply migration on a dev DB: `npx nx run
twenty-server:database:migrate:prod`
- [ ] Regenerate frontend types: `npx nx run
twenty-front:graphql:generate --configuration=metadata`
- [ ] Create a Linear OAuth app at
https://linear.app/settings/api/applications/new with redirect URI
`<SERVER_URL>/apps/oauth/callback`
- [ ] Deploy + install `twenty-linear` on a workspace, paste the Linear
client id/secret into the app's variables
- [ ] Click "Connect Linear" in the app's settings tab → complete OAuth
→ verify `connectedAccount` row created with `provider = 'app'`
- [ ] Trigger `POST /linear/create-issue` with a valid teamId → verify
issue lands in Linear
- [ ] Disconnect → verify the row is deleted and (if Linear's revoke
endpoint is configured in the manifest) the revoke call fires
- [ ] Verify `/settings/accounts` does NOT show the Linear connection —
it appears only under the Linear app's settings tab

## Out of scope (deliberately)

- **Cron + per-user providers**: a cron-triggered function with a
per-user OAuth provider currently returns `CONNECTED=false` (no user
context). The follow-up design is `useOAuthForUser(name,
userWorkspaceId)` paired with a `POST /apps/oauth/connection-token`
endpoint, deferred to keep this PR focused.
- **Token encryption at rest**: tokens stored as plain `varchar`
matching the existing Google/Microsoft pattern. Worth a separate
cross-cutting PR.
- **Manifest endpoint pinning**: a malicious app upgrade could change
`tokenEndpoint` silently. Same trust model as logic-function source code
(which already runs arbitrary server-side); worth tightening across the
whole upgrade pipeline rather than just OAuth.
- **CLI helpers** (`twenty oauth show-callback-url`, `twenty oauth
connect`): manual setup for v1.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-04 11:26:34 +02:00
ff22988caf revert: Sentry #20064 + @sentry 10.27 (prod bisect) (#20221)
## Summary

Reverts **#20064** (`feat(sentry): propagate workspace context to all
spans`) and downgrades **@sentry** packages from **10.51** back to
**10.27** (reversing **#20149**), to validate in production whether
recent Sentry/instrumentation changes correlate with OTLP/metrics
issues.

## Changes

1. **Revert #20064** — removes `beforeSendSpan` from `instrument.ts`,
restores `WorkspaceAuthContextMiddleware` / `BullMQDriver` behavior, and
deletes the three `apply-workspace-sentry-*` utils added in that PR.
2. **Sentry versions** — `packages/twenty-server` (`@sentry/nestjs`,
`@sentry/node`, `@sentry/profiling-node`) and `packages/twenty-front`
(`@sentry/react`) set to `^10.27.0`; `yarn.lock` regenerated via `yarn
install`.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 11:09:48 +02:00
276d4f6e84 fix(code-interpreter): three correctness fixes for the TwentyMCP helper + tool output shape (#20103)
## Summary

Three independently-useful correctness fixes for the `code_interpreter`
tool, all surfaced while standing up a self-hosted code interpreter
against MCP. Each is isolated to one bug and applies regardless of
`CODE_INTERPRETER_TYPE`.

### 1. UI: unwrap `execute_tool` envelope when rendering
`code_interpreter` output

When `code_interpreter` is invoked through MCP's `execute_tool`
meta-tool, the result arrives wrapped: `{success, result: {stdout,
exitCode, files, ...}, ...}`. `ToolStepRenderer` reads `exitCode` at the
top level, which is `undefined` → the step renders as "Failed" even on a
clean `exitCode === 0`. Symmetric to the input-side unwrap that already
exists; the fix lifts `outputObj.result` when `rawToolName ===
'execute_tool'`.

### 2. Helper: route `TwentyMCP.call_tool` through `execute_tool` for
catalog tools

The `TwentyMCP` helper injected into every code-interpreter sandbox
exposes a `call_tool(name, arguments)` method. Direct MCP calls only
work for the 5 meta-tools (`get_tool_catalog`, `learn_tools`,
`execute_tool`, `load_skills`, `search_help_center`); the 250+ catalog
tools are accessed through `execute_tool`. Today
`twenty.call_tool('find_companies', {...})` raises "Unknown tool". This
commit detects catalog tools and auto-routes them through
`execute_tool`. It also flattens the nested `{catalog: {category:
[...]}}` shape returned by `list_tools()` and propagates `{success:
false}` envelopes as explicit exceptions (they were being silently
returned as dicts).

### 3. Prompt: stop the agent from hallucinating \`import twenty\`

Despite the helper being pre-injected, models frequently emitted
\`import twenty\` and crashed with \`ModuleNotFoundError\`. Two
contributing sources: the helper docstring did not explicitly say "do
not import," and the code-interpreter skill template's example block
referenced placeholder tool names. Fix: explicit "DO NOT import twenty"
block in the helper + rewritten skill examples using real tool names and
real response shapes.

## Test plan

- [ ] Existing \`code_interpreter\` tests still pass.
- [ ] Run a chat turn that invokes \`code_interpreter\` indirectly via
MCP \`execute_tool\` and confirm the UI no longer flips to "Failed" on
\`exitCode === 0\`.
- [ ] From inside the sandbox, run \`twenty.call_tool('find_companies',
{limit: 5})\` and confirm records return (was raising "Unknown tool").
- [ ] Confirm \`twenty.list_tools()\` returns a flat list, not the
nested \`{catalog: {...}}\` envelope.
- [ ] Trigger a tool error from inside the sandbox and confirm it raises
rather than returning a \`{success: false}\` dict.
- [ ] Ask an LLM to use \`code_interpreter\`; confirm it does not emit
\`import twenty\`.

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-05-03 11:16:53 +02:00
nitinandGitHub 4f439bbe43 [AI] Prefer batch tools in system prompts (#20173) 2026-05-01 14:55:15 +02:00
4fbd8b207d chore: sync AI model catalog from models.dev (#20178)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

This PR updates pricing, context windows, and model availability based
on the latest data.
New models meeting inclusion criteria (tool calling, pricing data,
context limits) are added automatically.
Deprecated models are detected based on cost-efficiency within the same
model family.

**Please review before merging** — verify no critical models were
incorrectly deprecated.

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-05-01 08:50:48 +02:00
f7d812fca6 fix: disable sync on seeded message and calendar channels (#20168)
## Summary

The dev seeder creates `ConnectedAccount` records with fake OAuth tokens
(`'exampleRefreshToken'` / `'exampleAccessToken'`) and points
`MessageChannel` / `CalendarChannel` records at them with
`isSyncEnabled: true`. When the sync cron jobs run in the demo
workspace, they:

1. Pick up these seeded channels (filter is `isSyncEnabled: true` +
pending sync stage)
2. Try to refresh the fake OAuth tokens
3. Mark the channels as `FAILED_INSUFFICIENT_PERMISSIONS`
4. Surface a "Sync lost with mailbox X — please reconnect" banner in the
UI

This banner appears every time the demo workspace is loaded, even though
nothing is actually broken.

## Fix

Set `isSyncEnabled: false` on all 12 seeded channels (6 message, 6
calendar). This is the canonical "don't sync this channel" mechanism —
the same state a real user lands in when they toggle sync off in account
settings.

## Why this approach

- **ConnectedAccount records stay**: the demo workspace still shows Tim,
Jony, Phil, Jane as having connected their email/calendar — realistic
- **Pre-seeded messages and calendar events stay visible**: those don't
depend on `isSyncEnabled`
- **Crons no longer pick them up**: they filter on `isSyncEnabled:
true`, so `false` short-circuits the entire sync attempt — no failure,
no banner
- **Semantically correct**: the seeded accounts have fake tokens that
were never going to sync successfully; `isSyncEnabled: true` was
effectively a lie
- **No production code touched**: no `isDemo` flags, no magic-string
detection, no workspace-ID filters in the cron path

## Alternatives considered and rejected

- **Add an `isDemo` flag**: schema change, leaks demo knowledge into
production tables
- **Skip channels with fake tokens (`example*` pattern)**: hacky
magic-string detection in the auth refresh path
- **Filter demo workspace IDs in the cron**: production paths shouldn't
reference demo IDs
- **Don't activate demo workspaces**: breaks the demo workspace UX
entirely

## Test plan

- [ ] Reset the database and reseed (`npx nx database:reset
twenty-server`)
- [ ] Load the demo workspace — confirm no "Sync lost with mailbox"
banner appears
- [ ] Confirm seeded connected accounts still show in Settings →
Accounts
- [ ] Confirm pre-seeded messages and calendar events still appear in
the UI
- [ ] Confirm a real connected account (added via OAuth) still syncs
normally — its channel will have `isSyncEnabled: true` and the cron will
pick it up

## Related

Companion to https://github.com/twentyhq/twenty/pull/20167, which
removes the `--dev-mode` cron filter that was masking this banner issue
in the dev image.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-01 07:52:01 +02:00
c3a320c27b fix: register all cron jobs in twenty-app-dev image (#20167)
## Summary

The `twenty-app-dev` Docker image previously passed `--dev-mode` to
`cron:register:all`, which skipped all calendar, messaging, and workflow
sync cron jobs (only 4 generic crons were registered). This caused
periodic sync to silently stop after the initial import for community
members using the dev image as their actual instance.

## What changed

- Removed `--dev-mode` flag from
`packages/twenty-docker/twenty-app-dev/rootfs/etc/s6-overlay/scripts/register-crons.sh`
so the dev image registers all cron jobs (matching production behavior)
- Removed the now-unused `--dev-mode` option, `DEV_MODE_COMMANDS` set,
and conditional filtering logic from `cron-register-all.command.ts`

## Why this is safe

- **No log noise**: cron jobs gracefully no-op when no connected
accounts exist — they query for pending channels, find zero, and exit
early
- **No false banner**: the "reconnect account" banner only shows when a
user explicitly connected an account whose OAuth later fails, which is
correct behavior. No seed/demo data creates connected accounts, so a
fresh dev instance won't see any banner
- **Hiding crons just hid the symptom**: silently breaking sync with no
user feedback is worse than showing the banner if OAuth is misconfigured

## Context

Surfaced by a community member who reported that calendar sync cron jobs
never appeared in the queue after restarting the dev image, and only the
initial import worked. `--dev-mode` was added in #19138 as an
optimization for development but it doesn't match how the dev image is
actually used by community members deploying Twenty.

## Test plan

- [ ] Build/run the `twenty-app-dev` image
- [ ] Confirm worker logs show all cron jobs registering (calendar,
messaging, workflow, etc.)
- [ ] With no connected accounts: confirm no errors or log noise
- [ ] With a connected Google calendar: confirm periodic sync triggers
after ~5 minutes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-01 07:51:21 +02:00
WeikoandGitHub 85752f8a61 Bump 2.3.0 (#20169) 2026-04-30 19:34:43 +02:00