Commit Graph

5449 Commits

Author SHA1 Message Date
Abdul Rahman 148dc6dfaa Let server route resolvers answer the caller synchronously (#23233)
## Problem

A server route resolver can only return a dispatch target (`{
workspaceId, targetLogicFunctionUniversalIdentifier, payload }`), and
`ServerRouteTriggerService` always acks `202 {queued:true}`. The target
function runs off the queue, after the response has been sent, so its
return value can never reach the caller.

That makes it impossible to integrate a provider whose webhook URL has
to be proven with a handshake on the same response. Slack's Events API
is the case that surfaced it: `url_verification` sends `{ type,
challenge }` and will not accept the Request URL unless the challenge
comes back on that POST.

## Change

A resolver may now return a `Response` (the existing
`LogicFunctionHttpResponse`) instead of a dispatch target. The route
sends it as-is via `buildRouteTriggerResponse` and enqueues nothing.

- Reuses the marker and builder that HTTP route triggers already use, so
there is no new response shape.
- Dispatch results behave exactly as before; the resolver error path is
unchanged, just hoisted out of `parseResolverResult` so it runs before
the branch.
- SDK: `ServerRouteResolverResult` becomes `ServerRouteDispatchResult |
LogicFunctionHttpResponse`.

Additive: a resolver that returns a dispatch target sees no behavior
change. Previously, returning this shape threw
`RESOLVER_INVALID_RESULT`.

## Testing

`server-route-trigger.service.spec.ts` gains a case asserting the
resolver's response is sent verbatim and nothing is enqueued. 15/15
pass.

## Context

Split out of #22984 (Slack conversational assistant), which needs this
to complete the Slack Events URL verification. That PR depends on this
one merging first.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23233?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-24 08:56:34 +02:00
github-actions[bot] 5bc97f8591 chore: sync AI model catalog from models.dev (#23242)
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.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23242?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-07-24 08:47:03 +02:00
github-actions[bot] 34c2e11dcb i18n - translations (#23230)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23230?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-24 02:37:41 +02:00
Abdul Rahman 2c79093b74 feat: agent roleUniversalIdentifier for manifest-driven role assignment (#23206)
## Summary
- Adds optional `roleUniversalIdentifier` on `AgentManifest` /
`defineAgent` so apps can declaratively assign a role to an agent (same
config shape as `defaultRoleUniversalIdentifier`).
- Wires `agentUniversalIdentifier` as a sync many-to-one FK on
`roleTarget`, and emits a deterministic `roleTarget` from the agent
during app sync (create / update / delete).
- Enables app agents (e.g. Slack assistant) to get a role on install
without postInstall hooks or manual admin assignment.



<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23206?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-24 05:57:40 +05:30
neo773 59eead238d message list member backfill (#23176)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23176?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-24 00:55:46 +05:30
twenty-pr[bot] 6623901eb4 chore: bump version to 2.25.0 (#23221)
## 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
- Bumps twenty-client-sdk, twenty-sdk, and create-twenty-app to the same
version

## Checklist

- [ ] Verify version constants are correct
- [ ] Verify npm package versions match

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23221?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com>
2026-07-23 19:50:34 +02:00
Paul Rastoin b91c2a6457 fix(server): repair missing applicationRegistration.logoFileId on upgraded instances (#23215)
## Problem

closes https://github.com/twentyhq/twenty/issues/23210

Self-hosted instances on 2.23.x fail their workspace upgrade with:

```
column ApplicationEntity__ApplicationEntity_applicationRegistration.logoFileId does not exist
at UpgradePeopleDataLabsApplicationCommand.runOnWorkspace
```

The `2-21` instance command that adds
`core."applicationRegistration"."logoFileId"` was merged ~20 minutes
after the 2.22 version bump (PR #22827, `94192a2164`), so it first
shipped in 2.22 while registered under
`@RegisteredInstanceCommand('2.21.0', ...)`.

The upgrade runner resolves its start position from the last recorded
command and only moves forward. Any instance that had already run a
2.21.x binary has its cursor past that slot, so the command is skipped
permanently and the column is never created.
`UpgradeAwareEntityMetadataAdapter` decides column visibility
positionally (`index < currentCursor`), not by whether the command
actually ran, so it keeps `logoFileId` in the SELECT list and the
instance reports "Up to date" while the column is absent.

**Affected:** instances that ran 2.21.x, then upgraded to >= 2.22.
Instances that went from <= 2.20 straight to >= 2.22 replayed the full
sequence and are fine.

`logoFileId` is populated lazily by design (NULL is a supported state),
so no backfill is added.

## Changes

**1. Idempotent DDL guard in the failing workspace command**

`2-23-workspace-command-...-upgrade-people-data-labs-application.command.ts`
now ensures the column exists at the top of `runOnWorkspace`, before the
`findOne` that crashes on affected instances. It uses the core
`DataSource` (`@InjectDataSource()`) because
`core."applicationRegistration"` is instance-global, guards with a
per-process boolean in addition to the SQL-level `IF NOT EXISTS`, and
copies the full statement list (column + unique + FK constraints)
verbatim from the 2.21 command. In dry-run it probes
`information_schema.columns` and returns instead of running the crashing
query.

**2. Fast instance command in 2.23**
New
`2-23-instance-command-fast-1784823473532-add-logo-file-id-to-application-registration.ts`,
registered at the end of the 2.23 fast segment (highest timestamp),
running the same idempotent DDL. This covers the normal 2.22 -> 2.23
path and, critically, instances with zero provisioned workspaces where
the workspace command body never executes. The shared DDL lives in
`2-23/utils/ensure-application-registration-logo-file-id-column.util.ts`
so both paths stay byte-for-byte identical. Class name follows the
`Early2_4` / `Early2_5` precedent to avoid colliding with the 2.21
command.

The fix lives entirely in 2.23: instances stuck at the failing workspace
command retry it every run, and 2.22 -> 2.24 jumps still replay the 2.23
segment.

## Ops note

Instances failing right now can be unblocked immediately by running the
same `ALTER TABLE` block by hand against their core database
(byte-for-byte what the command does). Worth including in the 2.23 patch
release note.

## Verification

- New fast instance command re-slotted last in the 2.23 fast segment
(timestamp `1784823473532` > current max `1784659343818`).
- Manual repro path: boot `twentycrm/twenty:v2.21`, seed, stop, run
`upgrade` from this branch, assert the column exists and
`upgrade:status` reports 0 failed. The default v1.22 baseline does not
reproduce it (replays from cursor 0).

---
_Generated by [Claude
Code](https://claude.ai/code/session_01YAuDR585cx7FyAKoiT32j3)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23215?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: Paul Rastoin <paul.rastoin@gmail.com>
2026-07-23 17:12:32 +00:00
Thomas Trompette 5160415f40 fix(workflow): create core mirror rows during workflow prefill (#23204)
## Problem
Seeded workflows are inserted during workspace activation (and dev
seeding) via raw SQL in `prefill-workflows.util.ts`, which bypasses the
ORM entirely. The async dual-write listener that mirrors `workflow` /
`workflowVersion` into `core.workflow` / `core."workflowVersion"` never
fires for these rows, so every newly activated workspace is born with:
- no core mirror rows, and
- NULL `coreWorkflowId` / `coreWorkflowVersionId` soft-refs.

This is permanent, growing drift. The core-consistency check reports
every new workspace as 2 unlinked workflows + versions, and once trigger
dispatch reads from core these seeded workflows would silently stop
working.

## Change
Insert the `core.workflow` and `core."workflowVersion"` mirror rows and
stamp the soft-refs inside the same prefill transaction. Field mapping
mirrors the dual-write / backfill exactly:
- `core."workflowVersion".workflowId` = the workspace workflow id (what
the trigger-map cache groups by)
- `triggers` = jsonb `[trigger]`
- `applicationId` = `workspace.workspaceCustomApplicationId` (throws if
missing, same contract as the sync path)
- `core.workflow.lastPublishedVersionId` = the workspace version id
- one ACTIVE core version per workflow (satisfies the partial unique
index)

Core ids are deterministic v5 (same helper/namespace as the existing
prefill ids) so the existing `.orIgnore()` re-run guard stays idempotent
— a re-run hits the PK conflict and is skipped instead of inserting
duplicate orphan core rows.

The large diff is mostly re-indentation: the step/trigger arrays were
extracted to consts so the same objects feed both the workspace and core
inserts. The step/field UUID literals are unchanged from main.

## Verification
- `nx lint:diff-with-main twenty-server`: clean.
- Typecheck (isolated `tsc`): no errors in the changed file (`nx
typecheck twenty-server` is blocked by a pre-existing `twenty-shared`
build error on main, unrelated).
- Runtime: pending a `database:reset` + cross-schema parity query
confirming zero drift on a freshly seeded workspace.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23204?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-23 16:15:05 +00:00
martmull 4d09c400a4 Remove DATABASE_EVENT_JOBS_CHUNK_SIZE and Promise.all from logic function trigger jobs (#23205)
## What

- `LogicFunctionTriggerJob` now processes a single
`LogicFunctionTriggerJobData` payload instead of an array processed with
`Promise.all`.
- Removed `DATABASE_EVENT_JOBS_CHUNK_SIZE` and the `lodash.chunk` usage
in `CallDatabaseEventTriggerJobsJob`.
- Added `bulkAdd` to `MessageQueueService` and both drivers (BullMQ
driver uses native `queue.addBulk`, sync driver processes payloads
sequentially). `CallDatabaseEventTriggerJobsJob` uses it to enqueue all
payloads in one call.
- Updated the other producers (`ServerRouteTriggerService`,
`ApplicationInstallService`, `ConnectionProviderOauthFlowService`,
`CronTriggerCronJob`) to enqueue a single payload instead of a
one-element array, and updated the corresponding specs.

## Why

Each logic function execution now gets its own queue job, so a failing
execution only retries itself instead of re-running the whole chunk, and
job-level retry/metrics apply per execution.

---
_Generated by [Claude
Code](https://claude.ai/code/session_018VCs2kopnDiZCL41eToxQF)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23205?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-23 17:50:43 +02:00
Thomas Trompette 96a2456367 feat(workflow): periodic core-consistency check for workflows, versions and triggers (#23103)
Monitoring for the soft-ref migration. The `workflow` /
`workflowVersion` dual-write into core is **best-effort** (async, not
transactional; failures only go to Sentry), so `core.workflow` /
`core.workflowVersion` can silently drift from the workspace source of
truth. This adds a periodic job that detects that drift across **all
three workflow entities** and emits it as metrics.

Supersedes the earlier inline shadow-parity approach that lived on this
branch — that only covered trigger dispatch and added a cache read +
diff to every cron tick and every DB-event batch (too much hot-path
overhead). This is broader and fully off the dispatch path.

## What
A cron (`cron:workflow:core-consistency-check`, every 3 hours, wired
into `cron:register:all`). Per run:
- **Bounded**: `SELECT DISTINCT "workspaceId" FROM core."workflow"` —
only workspaces that actually use workflows (skips the large majority).
- Per such workspace, emit a drift metric per `(entity, driftType)` —
detect-only, plus a triage log:
- **workflow** and **workflowVersion** — `unlinked` / `missingCore` /
`orphanCore` / `fieldMismatch`, via cross-schema `COUNT` aggregates
(core + workspace are the same DB, so indexed joins — no rows pulled
into JS).
- **automated triggers** — the `workflowAutomatedTrigger` table vs the
`workflowAutomatedTriggerMaps` cache: `inTableNotCache` /
`inCacheNotTable` / settings `mismatch`.
- Per-workspace failures are isolated (caught → Sentry) so one bad
workspace does not stop the sweep.

## Why it is efficient
Two central `core.*` queries + a few `COUNT` queries per
*workflow-using* workspace, on a relaxed cadence, off the dispatch path.
Shardable across ticks later if needed.

## Metrics
`workflow-core-consistency/{workflow,version,automated-trigger}/drift`
counters, attribute `driftType`. Dashboards: twentyhq/twenty-infra#800.

## Not in scope
Detect-only — no auto-heal (the existing backfill/rebuild command can
heal). No dispatch or flag changes.

## Test
- The consistency SQL (workflow/version sync counts, orphan counts,
trigger read) validated against a live workspace (it surfaced real drift
there — unlinked versions + an orphan core version). The whole
cron→service→SQL→metric pipeline is proven live: the cron is already
emitting real drift counters on a running server.
- Unit specs for the service (clean → no metric; per-entity drift per
dimension; per-workspace error isolation).
- Command boots and registers via `cron:register:all` (verified).
Typecheck + lint clean.
2026-07-23 12:30:12 +00:00
Weiko 66df0ac47c Switch application stop/start commands to Redis-backed global kill switch (#23202)
## Context

[#23183](https://github.com/twentyhq/twenty/pull/23183) introduced the
right enforcement point: every logic-function execution is rejected
centrally before consuming the shared workspace throttle when its
application is stopped.

However, its server-wide path reads PostgreSQL for every execution
attempt. A kill switch is most useful while an application is producing
abnormal load, potentially while PostgreSQL is already under pressure.
The enforcement mechanism should not add more database traffic in that
situation.

This state is also operational and temporary. It is used to troubleshoot
an application, not as durable application configuration.

## What this PR changes

- Uses one global Redis key per application universal identifier:

  ```text
  module:applications:kill-switch:{applicationUniversalIdentifier}
  ```

- Keeps the check in `LogicFunctionExecutorService`, before the
workspace execution throttle.
- Adds a 60-second process-local cache for both present and absent keys.
- Deduplicates concurrent cache refreshes, so an execution burst causes
at most one Redis read per application and process.
- Fails open when Redis cannot be read and caches that result for the
same minute, avoiding a Redis retry storm.
- Removes the database columns, upgrade command, workspace-cache
recomputation, registration lookup, and stop/start CLI commands
introduced by #23183.
- Keeps disabled queued executions non-retriable, without emitting one
warning for every skipped payload.

The switch is operated directly in Redis. For example:

```redis
SET module:applications:kill-switch:{applicationUniversalIdentifier} 1 EX 3600
DEL module:applications:kill-switch:{applicationUniversalIdentifier}
```

Any value means stopped; deleting or expiring the key means enabled.

## Why this is a better fit

| | #23183 | This PR |
|---|---|---|
| State | Durable PostgreSQL fields | Ephemeral Redis key |
| Server-wide hot path | PostgreSQL lookup per execution | At most one
Redis lookup per app/process/minute |
| Scope | Workspace and application registration | Application universal
identifier across all workspaces |
| Operational cleanup | Explicit start command | `DEL`, eviction,
restart, or operator-selected TTL |
| Database dependency during an incident | Required | None |

The trade-off is deliberate: a Redis change can take up to 60 seconds to
reach every process, and the switch is lost when the cache key
disappears. That is acceptable for a temporary troubleshooting control
and keeps the normal execution path inexpensive.

Existing in-flight functions are not interrupted. New direct or queued
executions are rejected when they reach the executor.
2026-07-23 12:29:22 +00:00
Weiko 8368dd41c4 Remove legacy FindAllViews response cache flush from migration runner (#23190)
The pattern-flush (a full Redis keyspace SCAN) was kept for exactly one
release after the metadata GraphQL caches were re-keyed on flat-map
hashes (#23164), as the only invalidation signal for old pods'
version-keyed FindAllViews entries during rolling deploys. With all pods
on hash-keyed caches, dependency-hash rotation in the cache key covers
both view and metadata changes, so the flush is redundant.

flushGraphQLOperation has no remaining callers, so it is deleted from
WorkspaceCacheStorageService as well. incrementMetadataVersion stays: it
still feeds the X-Schema-Version check.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23190?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-23 14:28:07 +02:00
Etienne 5c825e8712 fix(ai-chat): write the stream heartbeat before the DB claim (#23198)
## Problem

Answering an `ask_questions` (select) prompt sometimes killed the turn
with
"Failed to get response. The response was interrupted before it could
finish."
The answer was swallowed and Retry rewound the whole turn. It was
intermittent,
worse on long threads and when coming back from another tab.

Reported in [discord quality
issue](https://discord.com/channels/1130383047699738754/1526875783170097172).
Confirmed in prod: ~28
`ai_chat_turn_failed_total{failure_phase="interrupted"}`
over the last 7 days (the only failure phase firing), plus matching
`the thread no longer holds this claim` worker logs around the report
time.

## Root cause

A stream is tracked by two records: the claim (`activeStreamId` in
Postgres) and
the heartbeat (a Redis key refreshed while the worker runs).
`reapDeadStream`
treats "claim set but no heartbeat" as a crashed worker and kills the
turn.

On the answer path the ordering left a window where that was falsely
true:

1. `resolvePendingQuestion` writes `activeStreamId` to Postgres (claim
set)
2. `enqueueResumeStream` reloads the thread and runs
`loadMessagesFromDB`
(reads every message and part, signs a URL per file, hundreds of ms on
long threads)
3. only then `markClaimed` writes the heartbeat

Between 1 and 3 the thread looks dead to the reaper. Worse,
`question-answered`
was published inside that window, so the client refetched, and the
refetch's
`chatStreamCatchupChunks` query runs the reaper, racing the server into
its own
setup window. The keepalive reap tick could land there too.

## Fix

Enforce one invariant everywhere: the heartbeat exists before any DB row
carries
the `activeStreamId`, so "claim without heartbeat" can only ever mean a
genuinely
dead worker.

- New `answerPendingQuestionAndResumeStream` owns the answer flow:
`markClaimed`
first, then the DB claim, then enqueue, then publish `question-answered`
(moved
after the enqueue so client refetches can't race the setup, and so we
don't tell
  the client "answered" when the enqueue failed and rolled back).
- Both failure paths clean up: clear the heartbeat if resolving fails;
restore the
  pending question and clear the heartbeat if enqueueing fails.
- `tryClaimStream` (send / retry / queue-flush) reordered the same way:
heartbeat
  before the claim, cleared if the claim is lost.
- `releaseStreamClaim` now also clears the heartbeat so failed claims
leave no orphan key.

No grace period or schema change needed: the ordering closes the race
structurally.
The Retry-rewinds-the-turn behavior is unrelated and left as a separate
follow-up.

## Testing

- New `agent-chat-streaming.service.answer.spec.ts`: heartbeat marked
before the
claim, publish only after enqueue, both failure paths restore state and
clear the key.
- Extended `agent-chat-streaming.service.claim.spec.ts`:
heartbeat-before-claim
  ordering and key cleanup on lost claim / failed enqueue.
- Full ai-chat suite green (74 tests), lint and typecheck clean.

After deploy,
`sum(increase(ai_chat_turn_failed_total{failure_phase="interrupted"}[1d]))`
trending to zero confirms the fix.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23198?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-23 14:17:50 +02:00
Raphaël Bosi f5a9adcb76 Add post-onboarding AI chat setup behind a feature flag (#23120)
https://github.com/user-attachments/assets/fec7076f-4e46-4c39-84d7-68e4340244ac



After finishing onboarding, users now land in a full-screen AI chat that
helps them set up their workspace, instead of going straight to their
default view. The welcome overlay's title flies into the chat's first
message so the handoff reads as one continuous motion: the slide plays
alone, the title swaps in place pixel-exactly (a regular-weight clone of
the target line is crossfaded in mid-flight to morph the font weight),
then the rest of the text fades in.

All of it sits behind `IS_ONBOARDING_AI_CHAT_ENABLED` (default off, not
registered as a public flag). With the flag off, onboarding behaves
exactly as it does today — the welcome overlay still plays and the user
lands on their home view.

Layout follows the Figma: the nav drawer stays visible and the chat
renders in a panel-styled container with an "Onboarding" header,
matching the expanded side panel.

Also fixes two pre-existing bugs the feature surfaced:
- On billing instances the completion redirect raced the lazy
`PaymentSuccess` page, which silently skipped the welcome animation on
the no-card trial path. The redirect now defers while a checkout is
pending, and `PaymentSuccess` always confirms through
`useLoadCurrentUser` so freshly served feature flags are respected.
- `useDefaultHomePagePath` could conclude its `/settings/profile`
empty-workspace fallback from a transiently empty metadata store and
strand the user there; it now waits for both object metadata and
navigation menu items before deciding.

Reviewer notes:
- `AgentChatRuntimeEffects` no longer keys off side-panel state, so
`modules/ai` stops importing `modules/side-panel`. The two
visibility-scoped effects moved into `AiChatTab`.
- `/workspace-setup` is deliberately URL-addressable rather than
onboarding-only: the collapse control in the header is a general
expand/collapse toggle (paired with a new expand button in the side
panel top bar), and gating the route would break refresh and
browser-back. It is still authenticated-only.
- The design's second, LLM-authored paragraph is not implemented —
starting an assistant turn with no user message needs server-side work.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23120?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-23 12:16:11 +02:00
Weiko e15b9efc2d Deprecate workspace metadataVersion and stop consuming it in the frontend (#23189)
## Context

Follow-up to #23164. Now that the metadata GraphQL response cache and
the workspace SDL cache are keyed on flat-map hashes,
`workspace.metadataVersion` no longer drives any cache invalidation.
This PR is the next stage of retiring it: the frontend stops consuming
the field entirely, and the public GraphQL field is marked deprecated so
external API consumers get a migration signal.

## What changed

**Frontend stops consuming `metadataVersion`:**
- `userQueryFragment.ts` no longer selects the field.
- `currentWorkspaceState.ts` drops it from the workspace `Pick`.
- `apollo.factory.ts` no longer attaches the `X-Schema-Version` request
header.

Dropping the header retires the "your workspace has been updated, please
refresh the page" error rewrite on the server (it only fired when the
header was present, and only on requests that had already failed
validation). Metadata staleness detection is unaffected: the frontend
has been running on collection hashes plus SSE since the
minimal-metadata work, so that path stays intact. Stale clients now
surface a raw validation error instead of the friendly message, which we
consider an acceptable trade for deleting the mechanism.

**Server marks the field deprecated:**
- `workspace.entity.ts`: `@Field({ deprecationReason: 'No longer used
for metadata cache invalidation, will be removed' })`.

**Regenerated (CI-enforced surfaces):**
`twenty-front/src/generated-metadata`, and `twenty-client-sdk`'s
generated schema, which now carries `@deprecated(reason: ...)`. The
`admin` codegen config produced no changes.
`packages/twenty-sdk/generated` is intentionally untouched: no in-repo
command produces it, CI does not drift-check it, and its committed
snapshot lags the live schema, so regenerating it here would pull
unrelated schema drift into this PR; it will pick up the directive on
its next routine refresh.

## Deployment notes

- No ordering constraint with #23164: removing a field selection and a
request header is backward compatible against any server, and old
frontend bundles keep working during the rollout because the field still
exists and the server-side header check is still in place. Same release
is fine.
- The follow-up server cleanup (removing the `X-Schema-Version` check in
`use-graphql-error-handler.hook.ts`, the per-request `metadataVersion`
reads and seed in `middleware.service.ts`/`jwt-auth.guard.ts`, and the
REST heal block) must wait until the release containing this PR has
shipped, since a deployed frontend still selecting the field would break
`GetCurrentUser` if the field were removed first.

After that cleanup, the only remaining `metadataVersion` consumers are
the five pinned upgrade commands (2.8 through 2.20), which hold the
column and `WorkspaceMetadataVersionService` until that upgrade window
closes; the physical column drop then follows the two-phase pattern used
for `gridPosition`.

## Validation

- Server and frontend typecheck, lint, and format pass; the apollo
factory test suite passes unchanged (it fixtures the field but never
asserted the header).
- Live introspection against a server running this branch returns
`isDeprecated: true` with the reason on `Workspace.metadataVersion`.
- CI's pending-codegen check covers the regenerated surfaces
(`data`/`metadata`/`admin` configs and
`twenty-client-sdk:generate-metadata-client`).

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23189?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-23 09:57:17 +02:00
Scarab Systems d1c70ab0bf Fix dashboard record table widget aggregate persistence (#23008)
## Summary

- Update dashboard record-table widget aggregate changes to write into
the widget draft while page layout edit mode is active.
- Include `aggregateOperation` when saving record-table widget view
fields through `upsertViewWidget`.
- Persist aggregate operations server-side for widget view-field create,
update, and clear flows.
- Add frontend utility tests and backend integration coverage for widget
aggregate create/update/clear behavior.

Fixes #22934.

## Why

Dashboard record-table widgets use their own draft view state while a
page layout is being edited. The aggregate footer path was resolving
fields through the normal current-view flow and then trying to persist
immediately, which can miss widget draft fields and fail before the save
flow runs.

This change keeps aggregate edits in the widget draft during page layout
editing, then saves the aggregate operation with the rest of the widget
view configuration.

## Validation

- `npx nx lint twenty-front`
- `npx nx typecheck twenty-front`
- `npx nx test twenty-front --configuration=ci`
- `npx nx build twenty-front`
- `npx nx build twenty-server`
- `npx nx lint twenty-server --configuration=ci`
- `npx nx typecheck twenty-server`
- `npx nx test twenty-server --configuration=ci`
- `npx nx jest --config ./jest-integration.config.ts --logHeapUsage
--runTestsByPath
test/integration/metadata/suites/view/upsert-view-widget.integration-spec.ts`
- `git diff --check`

Disclosure: I used AI-assisted coding tools while preparing this PR. I
reviewed the changes myself, tested them, and take responsibility for
the implementation and any follow-up revisions needed.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23008?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-23 08:07:50 +02:00
github-actions[bot] e2ea82170c i18n - translations (#23191)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23191?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-22 20:38:44 +02:00
martmull a3f4acadb1 Add workspace and server level stop commands for applications (#23183)
## Context

When an installed application misbehaves (e.g. a logic function loop
DDoSing the server or the database), we currently have no targeted way
to shut it down in production: the only kill switch is
`LOGIC_FUNCTION_TYPE=DISABLED`, which disables logic functions for the
whole instance. This PR adds an emergency stop mechanism at two levels:

- **Workspace level**: stop one installed application in its workspace.
- **Server level**: stop every application installed from an
`applicationRegistration`, across all workspaces.

## How it works

**New nullable `stoppedAt` columns** on `core.application` and
`core.applicationRegistration` (fast instance command `2.24.0`, with
`up`/`down` and `@WasIntroducedInUpgrade` decorators on the entities).

**Enforcement in a single choke point**:
`LogicFunctionExecutorService.execute()` is the funnel behind every
execution path (public route triggers, server route triggers, cron
triggers, database event triggers, workflow actions, agent tool calls,
manual GraphQL execution, install hooks). A new
`assertApplicationNotStopped` guard runs right after the flat entities
are resolved and throws `LOGIC_FUNCTION_DISABLED` (already mapped to a
403 on route triggers and handled by the GraphQL exception handler)
when:
- `flatApplication.stoppedAt` is set (workspace-level stop, read from
the cached flat application maps: zero extra runtime cost), or
- the linked registration is stopped (one indexed PK lookup, same
pattern as the existing per-execution server-variable query).

**Propagation**: the workspace-level stop invalidates and recomputes
`flatApplicationMaps` for the workspace, so all server instances pick
the flag up within the local cache TTL (100ms). The registration-level
flag is read live, so it is effective immediately.

## Ops commands

```bash
# Workspace level
yarn command:prod application:stop -a <application-id>
yarn command:prod application:start -a <application-id>

# Server level (all applications of the registration, all workspaces)
yarn command:prod application-registration:stop -r <application-registration-id>
yarn command:prod application-registration:start -r <application-registration-id>
```

Each command logs what was stopped/started and, for registrations, how
many installed applications are affected.

## Notes

- Stopped executions fail fast at the guard, so queued trigger jobs
(cron/db-event) burn a negligible amount of work while stopped.
- The two flags are independent: lifting a registration-level stop does
not clear workspace-level stops that were set individually, and vice
versa.
- Unit tests added for `ApplicationStopService`.


---
_Generated by [Claude
Code](https://claude.ai/code/session_01CjEnKUACn89aSgK1wEMH2d)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23183?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-22 18:30:56 +00:00
martmull a65da48591 feat(server): per-worker queue filtering via env vars (#23181)
## Context

Follow-up to #23134. Goal: stop a heavy/long-running queue from
saturating every worker pod and blocking the whole job pipeline, by
letting each BullMQ worker decide **which queues it consumes** from env
vars.

> Note: an earlier revision of this PR also added a dedicated
`application-queue` (concurrency 1). Per review, that was dropped —
application install/upgrade/backfill jobs stay on `workspaceQueue`. This
PR now contains **only** the worker queue-filtering mechanism.

## What changed

- The queue-worker explorer (`MessageQueueExplorer`, which only runs in
the `queue-worker` process) now reads two env vars before creating
workers:
- `WORKER_ENABLED_QUEUES` — comma-separated allowlist of queues this
worker processes (empty = all).
- `WORKER_EXCLUDED_QUEUES` — comma-separated denylist, applied after the
allowlist.
- Workers are only created for queues that pass the filter; filtered-out
queues are logged and skipped. Unknown queue names are logged as
warnings.
- Both vars are read directly from `process.env` (not the DB-backed
config-variable system), since they're worker-bootstrap settings.
- Pure decision logic + env parsing extracted to
`shouldCreateWorkerForQueue` / `parseQueueListFromEnv` utils with unit
tests.

Queue **clients** are still registered in every process, so jobs can be
enqueued from anywhere — only the **consumer** side is gated.

## Usage

Isolate `workspace-queue` (where the application jobs run) onto
dedicated pods:

- General worker pods: `WORKER_EXCLUDED_QUEUES=workspace-queue`
- Dedicated worker pods: `WORKER_ENABLED_QUEUES=workspace-queue`

## Tests

- `should-create-worker-for-queue.util.spec.ts`: allowlist / denylist /
precedence + env parsing.
- `typecheck` + `oxlint --type-aware` + `oxfmt --check` clean on the
diff.

## Companion

- twentyhq/twenty-infra#805 wires `WORKER_ENABLED_QUEUES` /
`WORKER_EXCLUDED_QUEUES` to the worker pods.

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

https://claude.ai/code/session_014XeN6wVSbMWnFu8jeLaSXk

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 19:28:17 +02:00
Weiko 0108a34765 Rekey metadata caches when flat map hashes change (#23164)
## Context

The `/metadata` GraphQL response cache (`ObjectMetadataItems`,
`FindAllViews`) and the workspace SDL cache were keyed on
`workspace.metadataVersion`, an integer bumped on every object/field
migration. That mechanism is legacy (the migration runner literally
calls it `getLegacyCacheInvalidationPromises`): the data plane already
moved to `WorkspaceCacheService`, which versions each flat entity map
with its own hash minted on invalidation.

Version keying had two concrete costs: the version bump was the only
proactive invalidation for `ObjectMetadataItems`, and keeping
`FindAllViews` fresh required `flushGraphQLOperation`, a full Redis
keyspace SCAN on every relevant migration. It is also the main blocker
for deprecating `metadataVersion` entirely.

This PR re-keys both caches on the flat-map hashes instead.

## What changed

**Response cache** (`use-cached-metadata.ts`): the key is now
`{operation}:{workspaceId}:{combinedDependencyHash}[:{userWorkspaceId}]:{locale}:{queryHash}`.
Each cached operation declares which flat maps its resolvers read
(`metadata-graphql-operations-to-cache.constant.ts`) plus a scope:
`ObjectMetadataItems` stays workspace-shared, `FindAllViews` is per-user
because unlisted-view visibility depends on the caller. When any
declared map changes, its hash rotates and the key rotates with it; no
flush needed. The key is resolved once per request and reused in
`onResponse`, so a rotation mid-request can never cache a response under
a fresher key than the data it was built from. If hash resolution fails,
the request is served uncached (Sentry-captured).

This also fixes three pre-existing key soundness gaps: `FindAllViews`
ignored locale although view names are translated server-side, the query
hash ignored GraphQL variables (`$viewTypes`), and mid-request version
rotation could re-key between request and response.

**SDL cache** (`workspace-graphql-schema-sdl.service.ts`): keyed on the
combined hash of the four maps the schema is generated from, taken from
the same `getOrRecomputeWithHashes` call that returns the data, so key
and content cannot skew. The `metadataVersion` read/seed block there is
gone; the Redis seed moved to `middleware.service.ts` so the
`X-Schema-Version` "refresh the page" check keeps working after the
Redis key's TTL expires.

**`WorkspaceCacheService`**: the internal pipeline now threads `{data,
hashes}` through every stage (local hit, hash validation, Redis fetch,
recompute) and the memoizer stores the pair, so returned hashes are
always consistent with returned data. New public
`getOrRecomputeWithHashes` and `getOrRecomputeCombinedHash`
(hashes-first: one MGET of the small `:hash` keys, full pipeline only
for missing ones, so cold pods never pull map payloads just to build a
key).

**Atomic pair writes** (`cache-storage.service.ts`): `mset` on the Redis
driver now delegates to the store's own `mset` (a MULTI of `SET ... PX`,
or native `MSET`), grouped by TTL. Previously it was a `Promise.all` of
independent SETs, so two concurrent recomputes could interleave and
leave one recompute's `:data` next to the other's `:hash`; with
hash-keyed caches that torn pair could persist a stale response under a
live key. `CoreEntityCacheService` writes through the same method and is
fixed for free.

**Cleanup**: `flush()` lost its `metadataVersion` parameter (always
pattern-flush per key on workspace deletion),
`METADATA_VERSIONED_WORKSPACE_CACHE_KEY` became
`HASH_KEYED_WORKSPACE_CACHE_KEYS` with the `MetadataVersion` key
relocated to `WORKSPACE_CACHE_KEYS` and the dead `ORMEntitySchemas`
entry removed.

## Deliberately unchanged

- `incrementMetadataVersion` and all its callers stay: the version still
feeds the `X-Schema-Version` check and the pinned upgrade commands.
Deprecating the column is a later stage.
- The runner's `FindAllViews` pattern-flush is kept for exactly one
release: view-only migrations never bump `metadataVersion`, so old pods
in a rolling deploy have no other invalidation signal for their
version-keyed entries. It gets deleted next release, which removes the
SCAN entirely.
- Old-shape cache entries are not migrated; they expire via the 7-day
TTL.

## Known limitations (follow-ups, not regressions)

- The plugin reads dependency hashes Redis-fresh while resolvers can
serve up to 10s-old memoized data, so a request landing right after a
migration can cache a pre-rotation response under the new key. Same
shape existed under `metadataVersion`; closing it needs request-scoped
snapshot plumbing.
- Concurrent recomputes are last-writer-wins (lost update). Fencing with
a conditional write is a follow-up.

## Validation

- Unit: response-cache plugin behavior (scope, key stash, serve-uncached
on failure, prototype-name guard), atomic `mset` batching, existing
`WorkspaceCacheService` spec passing unchanged.
- Integration: a new drift-guard spec runs the real
`ObjectMetadataItems`/`FindAllViews` operations with full frontend
selection sets against the in-process app, spies on
`WorkspaceCacheService`, and fails if resolvers read a flat map missing
from the declared dependency lists, so the constant cannot silently
drift.
- Manual against a live server: creating a field rotates the field-map
hash and the very next `ObjectMetadataItems` response contains it (hash
rotation is now its only invalidation path); warm hits are ~5ms; SDL
entries appear under hash-shaped keys via introspection.

## Suggested reading order

1. `workspace-cache.service.ts`, `workspace-cache-key.type.ts`,
`combine-cache-hashes.util.ts` (the `{data, hashes}` pipeline)
2. `use-cached-metadata.ts`,
`metadata-graphql-operations-to-cache.constant.ts`,
`metadata.module-factory.ts` (response cache)
3. `workspace-graphql-schema-sdl.service.ts`,
`workspace-cache-storage.service.ts` (SDL cache and renames)
4. `middleware.service.ts` (metadata version seed relocation)
5. `cache-storage.service.ts` (atomic writes)
6. Tests
2026-07-22 16:50:09 +00:00
Etienne f67e9c6b05 fix(ai-chat): disable Responses API storage for Azure models to stop "Item with id ... not found" stream failures (#23182)
## Problem

AI chat and workflow-agent turns on Azure-routed reasoning models (e.g.
`azure-foundry-us/gpt-5.5`) intermittently die mid-answer with:

> Failed to get response — `400 Item with id 'rs_...' not found`

## Root cause

The OpenAI Responses API is stateful: each output item gets a
server-side id (`rs_...` reasoning, `fc_...` function call). With
`store: true` (the API default), the Vercel AI SDK replays prior
assistant reasoning as `item_reference` entries that the provider must
resolve from its own storage. For reasoning models the chain-of-thought
is never returned in plaintext — it lives only as a stored referenced
item, or as `encrypted_content` requested via `include:
['reasoning.encrypted_content']` (which the SDK only auto-adds when
`store: false`).

PR #20888 (June 22) switched to the safe `store: false` mode, but only
for `AI_SDK_OPENAI`. `AI_SDK_AZURE` fell through to `default` and got no
options, so every Azure request ran in stored-reference mode. Azure's
item storage resolves those references unreliably (a server-side race —
the July 21 failure could not find the exact `rs_...` id Azure itself
had streamed seconds earlier in the same turn), and the failure is fatal
to the stream. Retries replay the same persisted references, so they can
fail again.

## Fix

Add an `AI_SDK_AZURE` case in `getCallLevelProviderOptions` that passes
`azure: { store: false }`. Both AI chat and workflow agents go through
this helper, so both paths are covered. The provider-options key for
`@ai-sdk/azure` is `azure` (its responses model is registered as
`azure.responses`); I verified the `@ai-sdk/openai@3.0.54` copy nested
inside `@ai-sdk/azure` has the same guards as the root `3.0.71`. With
`store: false` the SDK:

- auto-requests `include: ['reasoning.encrypted_content']` for reasoning
models (the `gpt-5.5` deployment name passes reasoning detection),
- replays reasoning as self-contained encrypted items instead of
server-side references,
- drops any stale unencrypted reasoning parts instead of referencing
them.

### Transition safety

Existing threads whose persisted reasoning has
`reasoningEncryptedContent: null` simply have those parts skipped on
replay. I checked prod logs for the related pairing-validation error
("was provided without its required...") and found zero occurrences
since OpenAI-direct made this same switch a month ago, so the transition
is safe.

## Testing

- Added two Azure cases to `provider-options.util.spec.ts` — all 9 pass.
- oxlint (type-aware) and oxfmt clean on the changed files.

## Post-deploy validation

Inverse of the evidence: new Azure reasoning parts should persist with
non-null `reasoningEncryptedContent`, and the Loki query below should go
quiet.


fixes :
https://discord.com/channels/1130383047699738754/1526873169124659230
2026-07-22 18:31:22 +02:00
Etienne c54ad3c0b9 feat(ai-instrumentation): fix AI histogram buckets & add tool-call duration instrumentation (#23173)
## What & why

Two related observability fixes for AI chat / agent / MCP metrics.

### 1. Widen histogram buckets (`widden-ai-histo`)

Latency percentiles for AI chat pinned at exactly **10s** on the
dashboard — not because anything times out, but because the histogram
buckets top out at 10,000.

`recordHistogram` created histograms without explicit bucket boundaries,
so the OTel SDK applied its defaults: `[0, 5, 10, 25, 50, 75, 100, 250,
500, 750, 1000, 2500, 5000, 7500, 10000]`. These were designed for small
generic values; interpreted as **ms**, the last finite bucket is exactly
10s. Every observation above 10s falls into the `+Inf` overflow bucket,
and quantile estimation clamps to the highest finite bound — so any
percentile that lands in the overflow draws a flat line at 10s.

Reality (gpt-5.5, last 30 days): 64% of turns exceed 10s (so even p50
pins at 10s), mean turn latency ~38s, max ~29min. The same 10k cap
affects the token-unit `tool-output-tokens` histograms.

**Changes:**
- `recordHistogram` now accepts an optional `bucketBoundaries`, applied
per-instrument via `advice.explicitBucketBoundaries` (supported in
`@opentelemetry/api@1.9.1`). Colocated with the metric, no
instrument-setup changes.
- Added bucket-boundary constants:
- `AI_LATENCY_MS_BUCKET_BOUNDARIES` — `[250, 500, 1000, 2500, 5000,
10000, 20000, 30000, 60000, 120000, 300000, 600000]` (250ms → 10min)
- `TOOL_OUTPUT_TOKENS_BUCKET_BOUNDARIES` — `[100, 250, 500, 1000, 2500,
5000, 10000, 25000, 50000, 100000, 250000, 500000]` (100 → 500k tokens)
- Wired boundaries into `ai-chat/turn-latency-ms`,
`ai-chat/step-latency-ms`, `ai-chat/ttft-ms` (latency) and
`ai-chat/tool-output-tokens`, `workflow-agent/tool-output-tokens`,
`mcp/tool-output-tokens` (tokens).

### 2. Instrument tool-call duration (`add-tool-duration-monitoring`)

Previously we tracked tool success/failure counts and output tokens, but
not how long each tool call took. Added a `*/tool-execution-duration-ms`
histogram for each execution path.

**Changes:**
- New metric keys: `ai-chat/tool-execution-duration-ms`,
`workflow-agent/tool-execution-duration-ms`,
`mcp/tool-execution-duration-ms`.
- New constant `TOOL_EXECUTION_DURATION_MS_BUCKET_BOUNDARIES` — `[25,
50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000, 60000, 120000]` (25ms
→ 2min).
- **AI chat / agent node**: use the AI SDK's
`experimental_onToolCallFinish` callback (`ai@6.0.97`), which reports an
exact `durationMs` measured around the tool's `execute()`, plus
`toolCall.toolName` and `success`. Attributes `{ model, tool }`.
- **MCP**: measured directly around `tool.execute` with
`performance.now()`, recorded on both success and failure paths.
Attributes `{ tool }`.

## Notes

- Backward-compatible: metrics without `bucketBoundaries` (e.g.
`job/latency-ms`, `sdk-client-generation/duration-ms`) keep OTel
defaults.
- **Historical data stays clamped** — only writes after deploy use the
new buckets, so percentile panels will show a step change at deploy
time. For truth on existing data, use a mean panel (Sum/Count is exact)
or Sentry trace span durations.
- Provider-executed tools (e.g. native web search) run inside the
provider, so they don't emit a local duration — same limitation as the
existing tool counters.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23173?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-22 17:51:06 +02:00
github-actions[bot] 0326e32b9b i18n - translations (#23184)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23184?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-22 17:45:03 +02:00
Abdul Rahman 04d1c2035c feat(connections): run a logic function on connection provider connect (#23167)
## What

Adds an optional `onConnectLogicFunctionUniversalIdentifier` field to
the connection provider manifest. When set, the referenced logic
function is dispatched right after an OAuth connection is successfully
established for that provider.

This gives apps a first-class "on connect" hook — e.g. the Slack app can
resolve the workspace's `team_id` via `auth.test` and claim the `team_id
-> workspaceId` mapping in the SERVER key-value store immediately on
connect, instead of racing against later events.

Follow-up to the app key-value store PR (#23089).

## How

- **twenty-shared**: add `onConnectLogicFunctionUniversalIdentifier` to
`ConnectionProviderManifest`.
- **twenty-sdk**: expose the field in `defineConnectionProvider` and
validate it is a UUID `universalIdentifier`.
- **twenty-server**:
- add a nullable `onConnectLogicFunctionUniversalIdentifier` column to
`ConnectionProviderEntity` (+ fast instance command / migration).
  - map the field through the

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23167?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-22 17:38:01 +02:00
Paul Rastoin 9043ab3091 Upgrade to 1.0.9 PDL app (#23172)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23172?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-22 15:02:42 +02:00
martmull f59bda1dbf feat(server): queued-only server-route dispatch (#23134)
## Context

Follow-up to the incident where 500s and latency spiked around 6pm until
the Recall webhook was disabled. Server routes
(`/webhooks/server/:resolverUid`) ran the resolver **and** the target
logic function synchronously inside the API request, so any handler
throw became a 500 and any slowdown past Svix's delivery timeout marked
the delivery failed — Svix redelivered, feeding load back into the API
in a self-sustaining storm.

## What changed

- Every server-route request acks with **202 `{ queued: true }`** as
soon as the resolver returns; the target runs on `logicFunctionQueue`.
Signature verification stays synchronous in the resolver and still
rejects with a non-2xx. External senders never observe target latency or
failures.
- The resolver contract is unchanged from main: `{ workspaceId,
targetLogicFunctionUniversalIdentifier, payload? }`.
- The target lookup still happens synchronously before enqueueing,
scoped to the resolver's application registration, so unknown targets
404 as before.
- Endpoints whose caller must read the response body (challenge
handshakes, Slack commands) should use `httpRouteTriggerSettings`
routes.

Final diff is 4 files: the server-route service, its spec, the
integration spec, and the docs page. Trigger jobs, fan-out, message
queue, shared types, and SDK are all untouched.

## Tests

- `server-route-trigger.service.spec.ts`: 202 ack + enqueue,
unknown-target 404 without enqueue, resolver auth/contract/error
mapping.
- Integration: `server-route-trigger-authorization.integration-spec.ts`
asserts the 202 queued ack (run locally against a seeded DB, green).
- `typecheck` + `lint:diff-with-main` + `oxfmt` clean.

## Notes

- **Breaking for existing server-route resolvers**: responses are always
202; the target's return value no longer reaches the caller. Existing
resolvers returning response bodies must move those endpoints to
`httpRouteTriggerSettings`.
- A queued target's handler failure is recorded in execution logs but
not retried (same as other queue-executed functions today); retry
semantics are deliberately out of scope here.
- Follow-up candidates: retry-on-failure semantics for queued
executions, `addBulk` for single-round-trip fan-out, declarative
signature verification to take resolver code out of the request path,
moving the call-recorder 250s artifacts import off the API request path.
- Companion PR #23135 (call-recorder): no app change needed for dispatch
— queued dispatch applies by default.
2026-07-22 14:57:18 +02:00
neo773 0489d39f2b fix(messaging): stop group and bulk emails from creating contacts (#23137)
Group addresses were only checked against the message sender, so
anything arriving as reply-to or cc slipped through and became a
contact. Now checked per participant at contact creation.

The group word list can't keep up with real senders (posts-recap@,
showinfo@, follow-suggestions@), so bulk-mail headers back it up:
`List-Unsubscribe, List-Id, Precedence, Auto-Submitted` (Industry
standard headers)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23137?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-22 17:17:45 +05:30
Thomas Trompette 25f28ee299 Fix TWENTY-SERVER-60Y: register permissions exception filter globally (#23104)
## Context

Sentry
[TWENTY-SERVER-60Y](https://twenty-v7.sentry.io/issues/6633503406)
("Permission Denied: Entity performing the request does not have
permission") has ~12.9k occurrences / 151 users. It groups by the shared
throw site `settings-permission.guard.ts:57`, so it's a **catch-all
bucket** for settings-permission denials across many resolvers, not a
single operation. Sampled events include `findOneApplication` (app
runtimes reading their own `applicationVariables`),
`uploadFilesFieldFileByUniversalIdentifier`,
`UpdatePageLayoutWithTabsAndWidgets`, `CreateFileUpload`, etc.

## Root cause

Settings-permission denials are only converted to a client-appropriate
`FORBIDDEN` when a resolver manually attaches
`PermissionsGraphqlApiExceptionFilter`. **28** GraphQL resolvers do;
**~35** guarded resolvers do not. On those, the guard-thrown
`PermissionsException` (a `CustomException`, not a `BaseGraphQLError`)
falls through to the Yoga error hook, is serialized as
`INTERNAL_SERVER_ERROR`, and `shouldCaptureException` reports it to
Sentry as a 500-class error.

REST is not affected: every `SettingsPermissionGuard` controller already
carries `PermissionsRestApiExceptionFilter` (15/15).

## Fix

Register `PermissionsGraphqlApiExceptionFilter` globally via
`APP_FILTER` in the core and metadata engine modules, mirroring the
existing global GraphQL filters `BillingGraphqlApiExceptionFilter` and
`FlatEntityMapsGraphqlApiExceptionFilter`. The filter is guarded on the
GraphQL context (`host.getType()`), so REST keeps its existing
per-controller filter untouched and no request-scoped dependency is
pulled into a global provider.

Every settings-guarded resolver now returns `FORBIDDEN` for denials,
which is both the correct client error code and excluded from Sentry.
Existing per-resolver
`@UseFilters(PermissionsGraphqlApiExceptionFilter)` entries remain valid
(handler-scoped takes precedence, identical result); collapsing them
into the global registration is a possible follow-up.

## Test

Added a case to `granular-settings-permissions.integration-spec.ts`: a
member without the `APPLICATIONS` flag calling
`findOneApplication{applicationVariables{key value}}` (the exact Sentry
query, and a resolver with **no** resolver-scoped filter) must receive
`FORBIDDEN`, exercising the global filter.

Verified against the local test DB:
- With the global filter: passes (`FORBIDDEN`).
- Without it: fails with `INTERNAL_SERVER_ERROR`, reproducing the leak.
- The existing roles / workspace-members / api-keys denial tests
(per-resolver filters) still pass, confirming no precedence conflict and
no boot issue from dual `APP_FILTER` registration.

Fixes TWENTY-SERVER-60Y
2026-07-22 12:10:49 +02:00
github-actions[bot] 9d4a564af4 i18n - translations (#23157)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23157?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-22 11:27:59 +02:00
Abdul Rahman 4c4a154d31 key-value storage for applications (#23089)
## What

Key-value storage for applications, as proposed in
twentyhq/core-team-issues#2391 — built on the existing `keyValuePair`
entity:

- Nullable `applicationId` relation on `keyValuePair` + a new
`APPLICATION_VARIABLE` type (fast instance command included)
- GraphQL CRUD on the metadata schema (`appKeyValue`, `setAppKeyValue`,
`deleteAppKeyValue`), requiring an `APPLICATION_ACCESS` token —
`applicationId` always comes from the token, never from arguments, so
apps can't touch each other's entries
- `kv.get` / `kv.set` / `kv.delete` helpers in
`twenty-sdk/logic-function`

## Scopes

- **`INSTALL`** (default): entries are private to one workspace install;
arbitrary JSON values
- **`GLOBAL`**: entries are shared across every install of the app, with
claim semantics — the value is always the claiming `workspaceId` and
only that workspace can overwrite or delete the key (guarded writes,
race-safe via insert-if-absent)

Since `applicationId` identifies an install (one row per workspace),
GLOBAL entries are stored under the registration owner workspace's
install so all installs of the same app share one namespace.

The GLOBAL scope is what enables cross-workspace webhook routing: e.g.
the Slack app's `serverRoute` resolver (running in the owner workspace)
can resolve `kv.get('slack:team:' + team_id, { scope: 'GLOBAL' })` to
find the workspace that connected that Slack team — without a workspace
being able to hijack another's mapping.

## Follow-ups

- Wire the Slack assistant PR (#22984) to write the claim at connect
time and read it in the events resolver
- `kv.*` access from front components

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23089?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-22 11:19:44 +02:00
twenty-pr[bot] 10a313dc7f chore: bump version to 2.24.0 (#23152)
## 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
- Bumps twenty-client-sdk, twenty-sdk, and create-twenty-app to the same
version

## Checklist

- [ ] Verify version constants are correct
- [ ] Verify npm package versions match

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23152?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com>
2026-07-22 10:48:10 +02:00
Paul Rastoin 893db7558e Fix instance fast migration, fallback index creation (#23149)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23149?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-22 10:22:41 +02:00
github-actions[bot] 7fa3c5c77b chore: sync AI model catalog from models.dev (#23143)
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-07-22 08:48:42 +02:00
Weiko 984944e9bc Fix workspace cache flush versioned metadata (#23121)
## Problem

`WorkspaceCacheStorageService.flushVersionedMetadata` never deleted
several of the keys it was supposed to flush:

- When called without a `metadataVersion` (the dev seeder path), it
built keys ending in `:*` and passed them to `cacheStorageService.del`.
`del` is an exact-key delete with no glob support, so this branch
deleted nothing at all.
- `setGraphQLTypeDefs` and `setGraphQLUsedScalarNames` can write
applicationId-suffixed keys
(`{key}:{workspaceId}:{metadataVersion}:{applicationId}`), which the
flush loop never matched. On workspace deletion these SDL and scalar
entries survived in Redis until the 1-week TTL expired.
- The `MetadataVersion` key is stored without a version suffix
(`metadata:workspace-metadata-version:{workspaceId}`), but the flush
loop appended `:{metadataVersion}` to every key, so it never matched
either.

## Fix

`flushVersionedMetadata` now targets the key shapes that are actually
written:

- The `MetadataVersion` key is deleted by its exact, unversioned shape.
- With a known `metadataVersion`, each versioned key gets an exact `del`
on `{key}:{workspaceId}:{metadataVersion}` plus a `flushByPattern` on
`{key}:{workspaceId}:{metadataVersion}:*` to catch
applicationId-suffixed entries. The pattern requires the trailing colon
so flushing version 1 cannot match version 12.
- Without a `metadataVersion`, each versioned key is flushed with
`flushByPattern` on `{key}:{workspaceId}:*`.

`flushByPattern` is Redis-only, which is safe here: the cache module
factory hardcodes the Redis store, and `flushGraphQLOperation` in the
same service already relies on it.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23121?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-22 01:28:19 +02:00
Etienne 5add5ea695 fix(billing) - expand billing sub uniqueness constraint (#23123)
fixes :
https://discord.com/channels/1130383047699738754/1528956737363771497

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23123?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-21 17:04:10 +00:00
github-actions[bot] af6f3c0c06 i18n - translations (#23126)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-21 19:00:53 +02:00
Weiko 28adcbffb9 Remove dead code from legacy metadataVersion cache mechanism (#23122)
- Drop unused setORMEntitySchema/getORMEntitySchema from
WorkspaceCacheStorageService
- Drop MetadataObjectMetadataMaps cache key, only referenced by the
flush loop
- Drop never-populated workspaceMetadataVersion field from workspace
auth context type and builders
- Drop unthrown TwentyORMExceptionCode.METADATA_VERSION_MISMATCH
- Drop unused WorkspaceMetadataVersionModule imports in field-metadata
and object-metadata modules

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23122?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-21 18:52:51 +02:00
Raphaël Bosi 578d69e4fb Fix update events reporting untouched columns as changed (#22797)
## Before


https://github.com/user-attachments/assets/b02dbd50-87c9-4699-a6a9-254a4c8b9182

The image uploaded during the onboarding is deleted and doesn't appear
in the animation

## After


https://github.com/user-attachments/assets/9443b837-044e-47c6-b2df-c392c238e935

The Image appears in the animation

## Description

TwentyORM's `.save()` emits UPDATE events whose `after` record carries
default (empty) values for columns that weren't written: TypeORM
null-injects untouched nullable columns on the entity in place, and
`formatResult` turns those into empty strings. So a partial update (e.g.
renaming a workspace member) reports untouched fields like `avatarUrl`
and `userEmail` as changed, which is what made the avatar-file-deletion
listener delete the picture uploaded during onboarding. The same stale
data also reaches webhooks, workflow/logic-function triggers and record
subscriptions.

Fix: `save()` was the only write path building its event `after` from
the in-memory payload instead of re-reading the row. `update()`,
`upsert()` and `softDelete()` all re-SELECT after the write, so `save()`
now does the same.

`withDeleted` is on both the before and the after find, since `save({
id, deletedAt })` and `save({ id, deletedAt: null })` are valid
soft-delete and restore, and an asymmetric find would leave a restored
row with no matching before-record.

Worth noting: a genuinely no-op save now emits no update event, where it
previously emitted one with a bogus diff.
2026-07-21 17:44:18 +02:00
Weiko 23cf85f745 Fix invalid UUID insert in workflow core-links backfill (#23118)
## Fix invalid UUID insert in workflow core-links backfill

### Problem
The `2-23:backfill-workflow-core-links` workspace upgrade command failed
with:

```
QueryFailedError: invalid input syntax for type uuid: "" (22P02)
```

The `core."workflow"."lastPublishedVersionId"` column is a `uuid`, but
some workspace workflows store an empty string `""` (not `NULL`) for
that field. The code used `workflow.lastPublishedVersionId ?? null`, and
`??` only falls back on `null`/`undefined` — so `""` was passed straight
through and Postgres rejected it.

### Fix
Use `|| null` instead of `?? null` so empty strings are normalized to
`null` before insertion into the `uuid` column.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23118?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-21 17:22:53 +02:00
Paul Rastoin 71a1ff7ac8 Cache twenty-client-sdk modules host-side via content-addressed URLs (#22981)
## Context

Front component sources are fetched host-side and integrity-verified by
the SHA-256 checksum embedded in their URL, cached in Cache Storage — a
layer that exists specifically because their download URLs are presigned
and rotate. The `twenty-client-sdk` modules (`core` and `metadata`) were
re-fetched on every render and could not be cached safely: their URLs
carried no checksum and the server exposed no freshness signal.

This PR makes the SDK module URLs **content-addressed** and relies on
the **browser HTTP cache** for immutability, and it keys the checksums
on their real owners: the **application** for `core`, the **instance**
for `metadata`. The checksum does double duty: cache invalidation
(regeneration changes the checksum → the URL changes → guaranteed cache
miss) and a server-side cacheability guard (the server only grants
`immutable` when the checksum in the URL matches the authoritative
checksum it knows for that module — persisted at generation time for
`core`, hashed once at bootstrap for `metadata` — so no per-request
hashing of the served bytes). Note this is **not** an end-to-end
integrity guarantee: there is no client-side hash verification, and on a
fingerprint mismatch the server still serves the current bytes with
`no-store` (self-healing for stale URLs) rather than failing.

<img width="2412" height="926" alt="image"
src="https://github.com/user-attachments/assets/d97935d2-0fdb-4c44-89ac-596b7ca8ca64"
/>

Closes twentyhq/core-team-issues#2688.

## Routes

| Module | URL | Scope |
| --- | --- | --- |
| `core` | `/rest/sdk-client/{applicationId}/core[/{checksum}]` | Per
application (generated bundle) |
| `metadata` | `/rest/sdk-client/metadata[/{checksum}]` |
**Instance-wide**: no application segment, so every application
converges on one URL and the browser downloads the module once per
release instead of once per application |

The previous application-scoped metadata path
(`/rest/sdk-client/{applicationId}/metadata[/{checksum}]`) is **kept for
backward compatibility**, new clients just stop generating those URLs.
The instance-wide route is declared before the parameterized route so
`metadata/{checksum}` is not swallowed as `:applicationId/:moduleName`.

## Caching model

| Request | `Cache-Control` | Effect |
| --- | --- | --- |
| Fingerprinted URL, checksum matches the known module checksum |
`immutable` | Cached indefinitely by the browser HTTP cache; a new
checksum is a new URL |
| Fingerprinted URL, checksum does not match | `no-store` | Current
bytes served uncached (self-healing for stale URLs) |
| Bare URL (pre-generation fallback, `core` only in practice) |
`no-store` | Never cached |

- Both responses also set `X-Content-Type-Options: nosniff` and
`Content-Type: application/javascript`.
- SDK modules are intentionally **not** placed in Cache Storage. That
layer stays reserved for the presigned/rotating component-source URLs;
SDK modules are served directly and authenticated, so the browser HTTP
cache (keyed by the content-addressed URL) is their single cache layer.

## Checksum provenance

- **core** — per **application**, persisted on
`application.sdkClientCoreChecksum` at generation time and read back
from `flatApplicationMaps` (never re-hashed per request).
- **metadata** — **instance-wide**, hashed once from the installed
`twenty-client-sdk/dist/metadata.mjs` package (warmed at bootstrap,
memoized per process) and served straight from that package, so it is
fresh from the first request after a release with no archive dependency.

## Server (twenty-server)

- Hash `dist/core.mjs` at SDK generation and persist
`sdkClientCoreChecksum` via `applicationRepository.update`. Adds the
nullable text column to `application.entity.ts` (mirroring
`packageJsonChecksum`) plus a fast instance command with up/down;
`FlatApplication` picks it up automatically.
- New **application-scoped** query
`applicationSdkClientChecksums(applicationId: UUID!):
SdkClientChecksums` on `ApplicationResolver` (metadata schema,
`WorkspaceAuthGuard` + `NoPermissionGuard`). `SdkClientChecksums.core`
is **nullable** and stays `null` until the SDK has been generated at
least once; `metadata` is **always present** (bootstrap-warmed), so the
metadata module is cacheable from the very first render of any app. The
query itself returns `null` only for unknown applications.
- `SdkClientChecksumsDTO` now lives in the shared
`core-modules/sdk-client/dtos/`. `FrontComponentDTO` and the
`frontComponent` resolver no longer carry checksums (decoupled from the
front-component row).
- `sdk-client` controller: instance-wide `metadata[/:checksum]` route
(no workspace-cache or application lookup, serves the memoized installed
module) + application-scoped `:applicationId/:moduleName[/:checksum]`
route (serves `core` from the per-application archive, `metadata` kept
for back-compat). Cacheability compares the URL checksum against the
**known** checksum — persisted `sdkClientCoreChecksum` for `core`,
memoized package hash for `metadata` — instead of hashing the served
bytes on every request: `immutable` on match, `no-store` otherwise (bare
URL or stale fingerprint), plus `nosniff`. A persisted checksum out of
sync with the archive only downgrades to `no-store` until the next
regeneration.

## Front (twenty-front)

- New metadata query `GetApplicationSdkClientChecksums`, keyed by
`applicationId`; removed the `sdkClientChecksums` selection from
`FindOneFrontComponent`.
- `getSdkClientUrls` builds the two module URLs independently:
`/sdk-client/{applicationId}/core/{checksum}` and the **instance-wide**
`/sdk-client/metadata/{checksum}` (no application segment → one shared
browser cache entry per release across all applications). Each falls
back to its bare URL when its checksum is absent — since `core` is
nullable, a never-generated app still gets a content-addressed metadata
URL and only `core` falls back. The checksum type is sourced from the
codegen `SdkClientChecksums` type rather than a hand-maintained
duplicate.
- `FrontComponentRenderer` is split into a gating outer component (runs
`FindOneFrontComponent`, renders nothing while loading) and a content
component that receives a guaranteed-non-null `frontComponent`.
Following project conventions, the side effects live in dedicated effect
components: `FrontComponentLoadErrorSnackBarEffect` (query error →
snackbar) and `FrontComponentApplicationTokenPairEffect` (mirrors the
query-derived token pair into component state unconditionally, `null`
included, so revoked credentials can never be retained or refreshed).
The content component fetches checksums via the application-keyed query
and **gates the mount of SDK-using components on that query**, so the
very first module fetch is always the content-addressed (`immutable`)
URL instead of the bare `no-store` one. Non-SDK components skip the
query and are never blocked.
- **Live invalidation without reload:** SDK regeneration updates the
application row, and the server broadcasts an `application` metadata
event carrying the new core checksum.
`useOnApplicationSdkClientChecksumsUpdated` /
`useUpdateSdkClientChecksumsApolloCache` patch the application-keyed
checksum query cache (core only; the instance-wide metadata is
preserved), so every mounted component of that application picks up the
new URL at once. This replaces the previous frontComponent-derived field
and closes the earlier "known gap" (a mounted component staying on a
session-old checksum until a full reload). The cache-patching callback
is memoized (`useCallback`) so the window listener is registered once
per application, and the listener is **skipped entirely** for non-SDK
components (`useListenToMetadataOperationBrowserEvent` gained a `skip`
option) — they register no listener and never refetch a query they don't
consume.

## Renderer (twenty-front-component-renderer)

- SDK sources are fetched through a dedicated plain authenticated fetch,
`fetchJavaScriptModuleSourceText` (Bearer header, `credentials:
'omit'`), instead of the Cache Storage `fetchComponentSource` path;
`fetchSdkClientSources` uses it. Execution stays exclusively in the
opaque-origin worker via blob URLs; the host only fetches and forwards
source strings (no hashing host-side). Staleness self-resolves through
the checksum: new checksum → new URL → cache miss.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22981?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-21 15:05:00 +00:00
Etienne 8f704e87e3 fix(ai-chat) - fix AI record references when display names contain markdown characters (#23113)
<img width="516" height="86" alt="Screenshot 2026-07-21 at 16 41 16"
src="https://github.com/user-attachments/assets/6e14b933-b48e-49ab-856b-400efdeba53a"
/>




## Summary
- Switch record references from `[[record:object:id:label]]` to
`[[record:object:id:label[[/record]]` so labels can include `]`,
backticks, brackets, and other markdown-significant characters
- Parse references with an explicit close tag (still accepting legacy
`]]`), escape labels before markdown lexing, and serialize mentions
through a shared formatter
- Update the AI chat system prompt so the model emits the new format

## Test plan
- [ ] Ask AI about a record whose name contains `` ` ``, `[`, `]`, or
`]]` and confirm it renders as a chip, not broken markdown
- [ ] Confirm legacy `[[record:...]]` references still chip correctly
- [ ] Mention a record in the chat editor and verify serialized text
uses `[[/record]]`
- [ ] Run:
- `npx jest src/modules/ai/utils/__tests__/findRecordReferences.test.ts
src/modules/ai/utils/__tests__/formatRecordReference.test.ts
src/modules/ai/utils/__tests__/protectRecordReferencesForMarkdown.test.ts
src/modules/ai/components/__tests__/TextWithRecordLinks.test.tsx
--config=packages/twenty-front/jest.config.mjs`
  - mention extension tests for `MentionTag` / `MentionSuggestion`

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23113?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-21 17:00:29 +02:00
martmull f5b06d20e4 Add per-application API rate limiter (#23100)
## Context

Application-token API requests are not rate limited today:
`throttleQueryExecution` in the common query runner only throttles
API-key requests, per workspace. An application installed on hundreds of
workspaces (Call Recorder is on 700+) can therefore hit the API with
large synchronized bursts, as seen with the recovery crons impacting
production.

## What changed

- New rate limiter in `CommonBaseQueryRunnerService`, applied when the
auth context is an application context, using the existing
`ThrottlerService.tokenBucketThrottleOrThrow` like the per-workspace
API-key throttle. Both REST and GraphQL record operations go through
this path.
- The limiter key is `api:throttler:application:{universalIdentifier}`
with no workspace component: the budget is shared by every installation
of the application on the instance, which is what protects production
from install-count-proportional load. `universalIdentifier` was chosen
over `applicationRegistrationId` because the latter is null for
unpublished/local applications.
- Two new config variables in the `RATE_LIMITING` group:
`APPLICATION_API_RATE_LIMITING_LIMIT` (default 500) and
`APPLICATION_API_RATE_LIMITING_TTL_IN_MS` (default 60000), i.e. 500
requests/min per application across all workspaces.
- Rejections raise the existing `ThrottlerException`, already mapped by
both the REST and GraphQL exception handlers, and increment a new
`common-api-query/application-rate-limited` metric (only for throttler
rejections, so cache infrastructure failures are not reported as rate
limiting).
- Cron trigger dispatch `retryLimit` raised from 3 to 10 so throttled
logic function executions eventually run once the budget refills.

The existing per-workspace API-key throttle is unchanged (extracted to
its own method).

## Notes

- The limit is tunable per environment without a deploy pipeline change.

## Test

- `npx nx lint:diff-with-main twenty-server` clean.
- `npx nx typecheck twenty-server` clean.
- Throttler spec passes (5 tests).

---------

Co-authored-by: martmull <martin@twenty.com>
2026-07-21 14:40:04 +00:00
martmull e14c32015f Make upgrade applications batch size a job parameter defaulting to 5 (#23101)
Makes the batch size used when upgrading applications a parameter
instead of a hardcoded constant, defaulting to 5, and lets admins set it
from the upgrade confirmation modal.

Backend:
- `UpgradeApplicationsJobData` gains an optional `batchSize` field,
passed through by `UpgradeApplicationsJob` to the service.
- `ApplicationUpgradeService.upgradeAllApplications` accepts an optional
`batchSize` parameter, defaulting to
`UPGRADE_APPLICATIONS_DEFAULT_BATCH_SIZE = 5` (previously a fixed batch
size of 20). The value is sanitized to a positive integer to avoid an
infinite batching loop.
- The `upgradeRegistrationApplications` admin mutation accepts an
optional `batchSize: Int` argument and forwards it to the job.

Frontend (admin panel):
- The "Upgrade existing installations" confirmation modal now includes a
"Batch size" number input, defaulting to 5, sent with the mutation.
- Updated the admin GraphQL document and generated types.

## Screenshots

Upgrade section on the admin app registration page:

![Upgrade
section](https://raw.githubusercontent.com/twentyhq/twenty/8f6f7b5b87b8218e10f9f8ed8a8b705cf25f22f6/upgrade-section.png)

Confirmation modal with the new batch size input (defaults to 5):

![Upgrade confirmation modal with batch size
input](https://raw.githubusercontent.com/twentyhq/twenty/8f6f7b5b87b8218e10f9f8ed8a8b705cf25f22f6/upgrade-modal-batch-size.png)

---------

Co-authored-by: Martin <martin@twenty.com>
2026-07-21 14:22:19 +00:00
Weiko 024b379e1a Instrument Node runtime and workspace cache metrics (#23107)
## Context

High API tail latency can come from either downstream work or the
Node.js process itself being unable to schedule work. Existing traces
expose database and HTTP spans, but they do not provide a continuous
event-loop signal or identify time spent rebuilding individual workspace
metadata cache entries.

## What changed

- Enable Sentry's built-in Node runtime integration with a 30-second
collection interval.
- Collect only event-loop delay p99, event-loop delay max, and
event-loop utilization. CPU, memory, p50, and uptime metrics remain
disabled because existing infrastructure telemetry already covers those
areas.
- Add a parent span around workspace metadata cache invalidation and
recomputation.
- Add child spans around cache-provider computation, including the cache
key, recomputation strategy, and whether the provider uses local data
only.

## Telemetry scope

- Cache hits do not create spans.
- Cache spans use `onlyIfParent`, so they are recorded only inside an
already-sampled trace.
- Runtime metrics are three low-cardinality values every 30 seconds per
server process.
- This does not add Prometheus histograms, per-cache-key metric labels,
database pool gauges, or a custom runtime collector.
- No cache behavior or invalidation semantics change.

This should let us distinguish event-loop stalls from downstream
latency, then identify which cache provider contributes to a slow cache
rebuild without materially increasing telemetry volume.
2026-07-21 14:02:58 +00:00
github-actions[bot] 64891c561d i18n - translations (#23108)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23108?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-21 15:48:30 +02:00
Félix Malfait 3ad3e8bd1a feat: kanban, calendar and group-by table layouts for dashboard view widgets (#22963)
## Context

Dashboard view widgets previously only rendered flat tables. This PR
ships the full feature: **Table with group-by**, **Kanban**, and
**Calendar** layouts for dashboard view widgets — server API + frontend,
end-to-end. (Originally staged as a 4-PR stack — #22966, #22967, #22968
— consolidated here per review.)

## Server / API

- **View typing.** Adds `KANBAN_WIDGET` and `CALENDAR_WIDGET` to
`ViewType` (following the `TABLE_WIDGET` precedent) so widget-backing
views keep their layout in `view.type` while staying excluded from
record-index pickers. Shared `getViewLayoutFromViewType()` maps widget
types to their base layout; `isWidgetViewType()` centralizes the
exclusions that were previously hardcoded per-site.
- **Migrations.** Two fast instance commands (**2.23**): `ALTER TYPE
core.view_type_enum ADD VALUE` for both values, and a widened
`CHK_VIEW_CALENDAR_INTEGRITY` constraint covering `CALENDAR_WIDGET`
(entity `@Check` updated for fresh installs).
- **Validation.** `FlatViewValidatorService` keys kanban/calendar
validation on the mapped layout, so widget views get the same invariants
as index views (kanban needs a groupable group-by field; calendar needs
a date field + layout). Calendar widget views default to month; a
non-month (DAY/WEEK) layout is rejected at the API level **unless** the
`IS_CALENDAR_WEEK_VIEW_ENABLED` feature flag is enabled for the
workspace — the same flag that gates day/week on index calendars.
- **API.** `upsertViewWidget` (LAYOUTS permission) accepts a nested
`view` settings input (`type`, `mainGroupByFieldMetadataId`,
`shouldHideEmptyGroups`, kanban aggregate/column-width, calendar
layout/fields). Routes through the standard update path, so `viewGroups`
auto-generate from SELECT options exactly like index views. Only widget
view types accepted; only `RECORD_TABLE` widgets can change view
settings.
- **AI tools.** `create-complete-dashboard` + `create_view` now
use/allow the `*_WIDGET` types (previously they created plain `TABLE`
views that leak into index pickers).

## Frontend

**Settings panel.** The **Source** (object) row comes first, since which
layouts are available depends on it. The **Layout** row below is a
working dropdown (Table / Kanban / Calendar); layouts the source object
can't support are **disabled with a hint** ("Needs a Select field" /
"Needs a Date field") rather than hidden. Group-by row (select fields;
searchable) with a **Hide empty groups** toggle while grouped; **Date
field** row replaces Group by while Calendar is active, and — when the
`IS_CALENDAR_WEEK_VIEW_ENABLED` flag is on — a **Calendar view** row
(Day / Week / Month) appears beside it; **Limit** row hidden while
grouped (only the flat virtualized loader enforces it). Kanban keeps its
group-by locked (no `None` option).

**Instant edit-mode preview.** Draft snapshots carry `viewGroups`;
picking a group-by synthesizes them client-side
(`buildDraftViewGroupsForFieldMetadataItem`, mirroring the server&#39;s
generation), so grouped tables/boards preview immediately before
dashboard save. On save, `upsertViewWidget` responses hand back the
server-generated groups, which replace the client-generated ones in the
persisted snapshot.

**Renderers.** `RecordTableWidgetRendererContent` branches on the
backing view&#39;s layout: `RecordBoardWidget` (wraps the standard
`RecordBoardContainer`) and `RecordCalendarWidget` (mounts the existing
`RecordCalendar`, which renders month / day / week) inside the same
per-widget provider sandbox the table uses.

**Read-only semantics.** Two flags with distinct scopes, each documented
on its state:
- `isRecordBoardViewSettingsReadOnlyComponentState` — locks the board
chrome that edits view settings (add group, column reorder/resize/menu,
aggregates); **card drag still updates records** under object
permissions.
- `isRecordCalendarReadOnlyComponentState` — widget calendars are
read-only by default (no drag, no add-new, no in-calendar layout
switch); cards open the side panel. The one exception, behind
`IS_CALENDAR_WEEK_VIEW_ENABLED`: a **live (non edit-mode) day/week**
widget calendar allows drag-to-reschedule and record creation under
object permissions. Month calendars and edit-mode previews stay
read-only.

**Calendar state componentization.** The calendar module&#39;s three
settings move from global atoms to component states keyed on
`RecordCalendarComponentInstanceContext` (same pattern as record-board),
so several calendar widgets and an index-page calendar can coexist
without leaking state. All readers resolve the ambient instance;
calendar unit tests updated.

**Multi-instance fixes that also fix index pages:** record drag states
were written against a different instance than every reader resolves
(now use the ambient instance); the board sticky-header DOM id is
namespaced per board; dragged board cards portal to `document.body`
while dragging so react-grid-layout&#39;s transforms can&#39;t offset
the clone from the pointer.

## Scope (v1)

- Widget calendars are month-only and read-only by default. With
`IS_CALENDAR_WEEK_VIEW_ENABLED` enabled, day/week layouts become
selectable (UI + API) and live day/week widget calendars support
drag-to-reschedule and record creation under object permissions.
- Widget group-by offers SELECT fields only (server auto-generates
groups from options; widgets have no per-record add-group flow).

## Tests

- Integration: `upsert-view-widget-view-settings.integration-spec.ts` (9
tests — group auto-creation, invalid type/field rejections, non-month
calendar widget rejected while the week/day flag is off and accepted
once it&#39;s enabled, combined settings+fields call); pre-existing
`upsert-view-widget` suite (20) green.
- Front: new suites for draft view-group generation and snapshot
clone/build utils; calendar suites componentized; full `twenty-front`
jest, typecheck, oxlint green; `twenty-server` typecheck + lint green.
- Browser-verified end-to-end (real dev server + seeded workspace):
configure → live edit-mode preview → save → reload for all three
layouts; measured drag with pointer inside the card; index-page calendar
re-verified (with the week/day flag enabled).

https://claude.ai/code/session_01E5N87kwwZWhDtEQaP72cMf
2026-07-21 15:41:08 +02:00
Paul Rastoin 2ef7b7824e Upgrade call-recorder, people-data-labs, last-contact and partners apps to twenty-sdk 2.23.0-alpha.1 (#23098)
## What

Upgrades the two breaking-change-prone apps to `twenty-sdk` /
`twenty-client-sdk` `2.23.0-alpha.1`, and adds the server-side hook that
lets the 2.23 upgrade install them:

- **people-data-labs**
- **partners**

Follows up on #22882 (System side effect relations), which re-derived
the system relation field universal identifiers name-free and shipped
`getSystemRelationFieldUniversalIdentifier` in the SDK.

## How

- **people-data-labs**: bump the SDK to `2.23.0-alpha.1`. The enriched
views temporarily hardcoded the new system relation identifiers with a
TODO because the SDK still embedded the old values; now that the
name-free identifiers ship in `2.23`, derive them from
`STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.{company,person}.fields.{noteTargets,taskTargets,attachments,timelineActivities}.universalIdentifier`
(identical to the previously pinned values, verified). Engine already
pinned `twenty >=2.23.0`; app stays `1.0.7` (manifest unchanged).
- **partners**: bump the SDK to `2.23.0-alpha.1`. The partner role
references
`opportunity.fields.{taskTargets,noteTargets,attachments,timelineActivities}`
universal identifiers, which the SDK now resolves to the `2.23`
name-free values. Pin `engines.twenty >=2.23.0` and bump the app to
`1.3.1`.
- **server**: add an opt-in `skipWorkspaceCompatibilityCheck` to the
install/upgrade path. The `upgrade-people-data-labs-application` 2.23
command runs mid-upgrade, before the workspace is marked as having
completed 2.23, so the workspace-compatibility check would otherwise
reject installing `1.0.7` (`engines >=2.23.0`). The server is already on
2.23, so the command passes the flag to install `1.0.7` and close the
desync window. Version-progression (downgrade/same-version) checks still
run.
- **call-recorder** and **last-contact** are intentionally left
unchanged (reverted): they don't define custom objects and don't
reference the system relation identifiers, so they aren't
breaking-change-prone and need no SDK bump.

## Breaking change constraints

- **people-data-labs** and **partners** reference system relation
identifiers that only exist on a `2.23` server, so both pin
`engines.twenty >=2.23.0`. Their `dockerhub-latest` integration leg is
red by design until a >=2.23 server image is published (same accepted
state as #22882); the `local` leg is green.

## Validation

- Regenerated the app lockfiles against the published `2.23.0-alpha.1`.
- `people-data-labs` typechecks cleanly against the real `2.23` SDK
types.
- CI: people-data-labs and partners green on `local`, red on
`dockerhub-latest` by design; server/SDK/all other checks green.
- Rebased onto latest `main`.
2026-07-21 15:40:14 +02:00
Marie bc3112a999 Fix: allow API key creation without Roles permission (#23102)
## Problem

A user with the **API keys & webhooks** permission but **without** the
**Roles** setting permission cannot create an API key through the UI.
The role selector relies on the `getRoles` query, which is guarded by
the `ROLES` permission, so the roles list comes back empty,
`SettingsDevelopersRoleSelector` early-returns, and no role can be
selected — leaving the form unsavable.

<img width="1058" height="408" alt="Screenshot 2026-07-21 at 13 38 34"
src="https://github.com/user-attachments/assets/fe97ba78-e116-458d-af10-11c5969c4636"
/>

## Fix

Expose the assignable roles through the API-key permission scope so
users can **pick** a role to assign to an API key without being able to
**edit** roles.

- **Backend**: add `getApiKeyRoles` query on `ApiKeyResolver` (already
guarded by `API_KEYS_AND_WEBHOOKS`), backed by
`ApiKeyRoleService.getApiKeyAssignableRoles` which returns roles where
`canBeAssignedToApiKeys = true`.
- **Frontend**: add a `GetApiKeyRoles` query and use it in the API key
create and detail pages instead of `getRoles`. The role selector prop
type is narrowed to the fields it actually uses.

<img width="1025" height="455" alt="Screenshot 2026-07-21 at 13 45 01"
src="https://github.com/user-attachments/assets/f1be8f97-5a30-4afc-9eee-c928f4607471"
/>
2026-07-21 13:31:05 +00:00
Marie e5fc5054cc Fix "Go to roles settings" command (#23105)
**Fix the "Go to Roles Settings" command** — it pointed at the
non-existent `/settings/roles` route and now navigates to
`/settings/members#roles`.

**Backfill existing workspaces** — added the
`upgrade:2-23:fix-go-to-roles-settings-command-menu-item-path` workspace
command, which rewrites the seeded command menu item payload for
existing workspaces. It is idempotent and only touches workspaces still
holding the legacy path.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23105?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-21 13:30:36 +00:00
Weiko 26227eff31 Add metadata cache tracing and avoid duplicate cache lookups (#23080)
## Summary
- Add Sentry tracing around metadata GraphQL cache reads with operation
tags and cache-phase attributes.
- Record cache hits on the request path so the response hook skips a
duplicate lookup after an early return.
- Cover the cache-hit, request-miss/response-hit, and allowlist
filtering cases with unit tests.

Before, even a cache hit performed two Redis reads:
```
onRequest  → GET → hit → return cached response
onResponse → GET → hit → do nothing
```
GraphQL Yoga still calls onResponse for an early cached response, so
that second lookup was redundant in most cases.
Now
```
onRequest  → GET → hit → mark request in WeakSet → return cached response
onResponse → request marked → remove marker → return immediately
```

onResponse cache mechanism is also there to prevent race conditions
like:
```
Did another request populate this key while I was executing?
  yes → keep it
  no  → cache my response
```
2026-07-20 19:22:57 +02:00