Commit Graph

14085 Commits

Author SHA1 Message Date
nitin f8ed432e2a Harden Fireflies call synchronization lifecycle (#23610)
Makes Fireflies call syncing (webhook and manual) resilient and
lifecycle-correct.

- Keeps a call recording `PROCESSING` until both transcript and summary
are filled, then marks it `COMPLETED`
- Looks up existing call recording field state first so only missing
fields are fetched from Fireflies
- Makes the call recording write race-safe: deterministic-id create with
a concurrent-create fallback
- Adds bounded retries with rate-limit handling to Fireflies API
requests
- Bumps twenty-sdk to 2.25.0 and validates query results with zod

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23610?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-31 11:26:46 +00:00
martmull cc9c6ad0ea Replace last-contact backfill with cursor-paginated per-record backfills (#23582)
## What

Replaces the single-pass last-contact backfill in the `last-contact` app
with three independent cursor-paginated backfills, one per object:

- `backfill-people-last-contact`
- `backfill-companies-last-contact`
- `backfill-opportunities-last-contact`

The post-install `backfill-last-contact` function now just dispatches
the three by posting to their own HTTP routes.

## How it works

Each backfill function:
1. Selects the first 20 records after the given cursor.
2. Computes the last-contact columns for each record, one by one, from
raw message and calendar interactions (people from their own
interactions, companies from the most recent contact of their people,
opportunities from their point of contact).
3. Updates each record individually.
4. Sleeps briefly, then re-triggers itself with the next cursor until
there are no more records.

Because every batch computes from raw interaction data, the three
backfills are order-independent and can run concurrently.

## Why

The previous backfill loaded everything and fired updates in bursts,
which hit hosted API rate limiting on large workspaces. Spreading
updates 20 records at a time with a pause between pages keeps the load
under the limit.

This is a temporary fix until the `enqueueJob` utility handles
throttling natively.

## Notes

- App version bumped to 1.1.4 so the upgrade hook re-runs on existing
installs.
- No tests added, per the temporary nature of the change.
- Typecheck and lint pass.

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23582?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-31 10:55:02 +00:00
github-actions[bot] 91c585196d i18n - docs translations (#23624)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-31 11:31:36 +02:00
github-actions[bot] e0af40bfa9 i18n - translations (#23621)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-31 11:21:54 +02:00
github-actions[bot] 17ddff8470 i18n - translations (#23620)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-31 11:15:28 +02:00
Thomas Trompette 3ed11054a0 Keep leading + when filtering phones by calling code (#23546)
Fixes #23528

Filtering a PHONES field with `CONTAINS` / `DOES_NOT_CONTAIN` stripped
every non-digit character from the filter value, so `+33` became `33`
and the generated `ilike`/`like` predicates could not distinguish an
international calling code from any number containing those digits.

`turnRecordFilterIntoGqlOperationFilter` now preserves a leading `+`
while still removing other formatting characters (spaces, dashes,
parentheses). `+33 6 12` becomes `+33612`; values without a `+` are
unchanged.

Added a regression test in `computeViewRecordGqlOperationFilter.test.ts`
for a `+`-prefixed value.

Lint and typecheck pass on `twenty-shared` and `twenty-front`; the
filter test suites pass in both packages.

---------

Co-authored-by: Thomas Trompette <tom@twenty.com>
2026-07-31 09:14:19 +00:00
Etienne 8f9f2f390e fix(ai-chat): show streaming activity during and between steps (#23581)
https://github.com/user-attachments/assets/e72e313c-66f8-40af-bf48-9225422ffa78




## Problem

During a streaming turn with tool calls, the chat goes completely static
in two places:

- **Between two steps**: once a tool's output arrives, its row flips to
past tense and nothing animates until the model's next chunk arrives (a
full LLM round trip, often several seconds). This window is defined by
the absence of parts, so no part-driven component can fill it — and the
pre-turn "…" indicator can't either, since it's cleared on the turn's
first chunk and never comes back.
- **During tool execution**: the active tool row in
`ThinkingStepsDisplay` is a static icon + label; the only animated
element there is the orbit loader on an actively-streaming reasoning
part.

Users can't tell whether the AI chat is still thinking or blocked.

## Fix

- **Pending thinking row between steps.** The renderer flags the
trailing thinking-steps group of a streaming, error-free message
(`showPendingThinkingRow`), and `ThinkingStepsDisplay` appends the
thinking row (orbit loader + "Thinking") inside its rows container when
none of its own steps is active (`isThinking`, which it already
computes). The row occupies the exact slot where the next real step row
materializes, so the handoff happens in place with no layout shift.
- **One shared row component.** `AiChatThinkingRow` renders the orbit
loader + "Thinking" and is used both for an actively-streaming reasoning
step and for the pending row.
- **Shimmer on executing tools.** Active tool rows wrap their label
("Searching the web for…") in the existing `ShimmeringText` while
awaiting output, with the text as a direct child of the background-clip
element so the effect applies reliably.
- **Activity derived from the tool lifecycle state.**
`isThinkingStepPartActive` now checks `input-streaming` /
`input-available` instead of output presence, so a tool completing with
a legitimate `null` output is no longer classified as still running.

Why the trailing-group check is sufficient: anything in progress outside
the group — streaming answer text, a running code execution card, a
pending question — is itself a later render item, so the group isn't
last and never gets flagged. No message-wide part scanning needed.

## Notes

- The row renders only while `agentChatIsStreaming`, which the existing
keepalive watchdog force-clears (with a visible connection-lost error)
after ~5s of subscription silence — it cannot spin forever on a dead
stream.
- It never shows while waiting on the user: `ask_questions` renders as
its own item after the group, and the server ends the stream on that
tool anyway (`stopWhen`).
- Consciously not covered, for simplicity: a pause right after a
mid-turn text part or right after the routing row.

## Tests

- Renderer: trailing group flagged as pending while streaming; not
flagged when answer text follows or when not streaming
- `ThinkingStepsDisplay`: pending row appended after completed steps,
suppressed while a tool step runs, loading label shown on a running tool
- `isThinkingStepPartActive`: lifecycle-state cases, including a
completed tool with `null` output

Lint, format, and `typecheck twenty-front` are clean.
2026-07-31 09:07:29 +00:00
Raphaël Bosi a9084604b4 Use the fast model for the onboarding setup chat (#23586)
The workspace setup chat ran on the smart model. The hidden kickoff turn
enqueued its job without a `modelId`, and the frontend sends none unless
the user picks one, so every turn fell through to `modelId ??
workspace.smartModel` in `chat-execution.service.ts`.

Two halves, since the kickoff is server-initiated and the frontend never
sends it:
- `startHiddenKickoffStream` takes a `modelId` and the setup chat passes
`workspace.fastModel`.
- `useAgentChatModelId` requests `workspace.fastModel` on the setup
page, so user turns follow. Everywhere else it still sends nothing and
the server fallback is unchanged.

`workspace.fastModel` defaults to the `default-fast-model` sentinel, so
the model still resolves through the registry and stays
admin-overridable. An explicit pick from the model picker still wins.
2026-07-31 08:21:02 +00:00
github-actions[bot] 8abc8f4bc9 i18n - docs translations (#23618)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23618?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-31 09:35:47 +02:00
Félix Malfait 7b7e4a5eca docs: fix inaccuracies found auditing the docs against v2.27.0 (#23616)
Prompted by user feedback: *"The documentation doesn't always reflect
the latest release. Some articles are outdated or incomplete."*

I audited every English page under `packages/twenty-docs` against the
code at v2.27.0, verifying each checkable claim (commands, env vars,
enum members, payload shapes, prop tables, API routes) against source in
`packages/`. Anything without a `file:line` citation proving the docs
wrong was dropped.

**Result: 414 findings across 226 pages — 75 critical, 169 major, 170
minor.** The feedback is accurate, and understates it in the
developer-facing sections.

This PR fixes a first slice. The full findings list is below so the rest
can be picked up.

---

## What this PR changes

**Removes the `twenty-ui` component reference** (25 English pages + 325
translations). The section predated the extraction of the design system
into the `twenty-ui` package:

- Not one import path resolved. `twenty-ui/display` and
`twenty-ui/components` are not export subpaths (real ones:
`data-display`, `feedback`, `icon`, `input`, `navigation`, `surfaces`,
`layout`, …), and ~20 more examples imported `@/ui/...` paths no longer
in twenty-front.
- Three documented components no longer exist: `SoonPill`,
`AutosizeTextInput`, `MenuItemCommand`.
- `Chip`'s props table documented the deleted `EntityChip`.
`ProgressBar`'s entire API was replaced
(`duration`/`delay`/`easing`/`barHeight`/`autoStart` →
`value`/`barColor`/`countdownDurationInMs`/…).

It was also unreachable from the navigation, so the pages were indexed
and searchable but maintained by nobody. Storybook is the live source of
truth here, which is why this is a deletion rather than a repair.

**Legal FAQ.** Corrects the workspace deletion timeline to match clause
4.9 of the DPA the product itself generates (~90 days from live systems,
a further ~90 for backups, isolated throughout) instead of the previous
claim of immediate removal with 7-day backup retention. Rephrases the
support-access answer to describe what the product actually does: access
is on by default and can be disabled in Settings → General → Security,
rather than the previous claim that it requires the customer to report
an issue and grant access.

**Self-hosting setup page.** The SMTP configuration block used
`<ArticleTabs>/<ArticleTab>`, leftovers from the pre-Mintlify site.
Those components are undefined here, so the Gmail/Office365/smtp4dev
instructions were not rendering at all. Converted to `<Tabs>/<Tab>`.

**Removes a fabricated Enterprise gate.** A Warning on the app
publishing page claimed cross-workspace sharing of tarball apps requires
an Enterprise key and that the Distribution tab shows an upgrade prompt.
No such gate exists in code, and its link target didn't exist either.

**Link and asset fixes.** Retargeted the two `docs.json` redirects whose
destinations 404'd; fixed the Code of Conduct link (file lives under
`.github/`); fixed the app-roles example link to
`examples/hello-world/src/roles/default-role.ts`; pointed the Contribute
frontend card, four `/developers/extend/apps/getting-started` links and
one `/twenty-ui/display` link at real pages; dropped two `<img>` tags
whose files are absent from the repo.

After this PR: every internal link and image reference resolves, all 171
navigation entries resolve to a file, and no redirect destination is
dead.

---

## Audit: what else is wrong

### Root causes

The failures aren't random rot. Four mechanisms produce nearly all of
them:

1. **Nothing links renaming a symbol to updating the page that documents
it.** Whole pages describe APIs returning zero grep hits:
`MessageQueueServiceBase`, `useScopedHotkeys`/`PageHotkeyScope`,
`@Gate`, `SoonPill`.
2. **"Coming soon" is written once and never revisited.** Nine features
are documented as unavailable that have shipped.
3. **Pages are dropped from navigation but left on disk.** 55 were
unreachable yet still indexed and searchable.
4. **Docs written from intent rather than from code.** One case is
provably born-stale: the `front-components` limitations table was
written in a commit that landed *after* the commit which polyfilled the
APIs it lists as unsupported.

### Priority 1: pages that actively break the reader

**Workflow template variables are wrong across 12 pages.** The largest
cluster — 16 critical findings, one root cause. Record-event triggers
expose the record under `properties.after`/`properties.before`; manual
triggers under `payload`; webhook triggers store the posted body flat
with no wrapper. Docs use `{{trigger.object.*}}`, `{{trigger.body.*}}`,
`{{trigger.subject}}` throughout. Search Records returns `{ first, all,
totalCount }`, not an array, and the resolver is Handlebars, which
doesn't accept `[0]` indexing at all — so `{{searchRecords[0].name}}`
and `{{searchRecords.length}}` cannot work. Iterator exposes
`currentItem`, not `item`/`index`. Evidence:
`generate-fake-object-record-event.ts:44-60`,
`workflow-schema.workspace-service.ts:501-517`,
`find-records.workflow-action.ts:111-117`,
`workflow-iterator-result.type.ts:2-3`,
`twenty-shared/src/utils/evalFromContext.ts`. Every workflow tutorial on
the site is copy-paste-broken. Highest-value fix in the audit, and
mostly mechanical.

**Self-hosting runbook commands don't work.** Backup names a container
and database that don't exist (service is `db` → `twenty-db-1`; database
is `default`, not `twenty`). Restore runs `docker compose stop
twenty-server twenty-front`, neither of which is a service — the compose
file defines `server`, `worker`, `db`, `redis`, and there's no separate
frontend service. The "unable to log in" fix runs `yarn` and `npx nx
database:reset` inside the production container, whose Dockerfile
deletes `npm`/`npx` and ships only `dist/`. Someone following the backup
page ends up with no backup.

**API, webhook and OAuth contracts are wrong.** The documented webhook
payload (`event`, `data`, `timestamp`) is not what the server sends —
the real body is `targetUrl`, `eventName`, `objectMetadata`,
`workspaceId`, `webhookId`, `eventDate`, `userId`, `workspaceMemberId`,
`record`, optional `updatedFields`
(`transform-event-batch-to-webhook-events.ts:34-46`). Any integration
written from that page fails to parse. `GET /oauth/authorize` doesn't
exist (server serves `/oauth/register`, `/token`, `/revoke`,
`/introspect`; authorization is served by the frontend at `/authorize`).
`/oauth/register` never returns a `client_secret` —
`token_endpoint_auth_method` is hard-coded `'none'` — so the documented
response and the "store it securely" warning are fiction, and the Client
Credentials section is unusable with a DCR client. PKCE is mandatory,
not "recommended". Batch limit is 200, not 60 (`QUERY_MAX_RECORDS =
200`), making the derived throughput estimates ~3.3x off.

**Contributor onboarding teaches removed APIs.** `queue.mdx`,
`hotkeys.mdx` and `feature-flags.mdx` are wrong at essentially every
step. Documented nx targets `twenty-server:database:migrate:prod`,
`twenty-server:test:unit` and `npx nx start` aren't real targets and
fail outright. `local-setup.mdx` never mentions
`packages/twenty-utils/setup-dev-env.sh`, the supported entry point.
Both style guides teach the `${({ theme }) => ...}` pattern, which now
returns **zero** hits in twenty-front against 929 files using
`themeCssVariables`. `frontend-commands.mdx` still lists Craco; the
frontend is Vite.

**SSO configuration is substantially fiction.** Twenty supports exactly
two protocols, OIDC and SAML. The docs omit OIDC entirely, present
Google Workspace and Microsoft Entra ID (separate social-login toggles)
as SSO providers, list configuration fields matching neither form, and
instruct the reader to click a **Test Configuration** button that exists
nowhere in the codebase.

**Data model.** The field-type table documents two types that don't
exist (`Domain`, `Long Text`) and omits three users can actually pick
(`Files`, `Full Name`, `Rich Text`). The filter-operator table is wrong
for every field type listed: Text has none of its four documented
operators, Date is missing six of nine.

**Import guidance that fails silently.** `DD/MM/YYYY` is documented as
supported; import uses plain `new Date(value)`, so `15/03/2024` is
always rejected and `03/15/2024` always read US-style — and the sibling
`fix-import-errors.mdx` says the opposite. The company sample CSV is
unusable as written (`Domain / Domain Label` headers don't exist; real
ones are `Domain Name / Link Label`).

### Priority 2: shipped features documented as unavailable

This is the specific complaint in the feedback. Each is a one-line fix.

| Documented as | Reality |
|---|---|
| AI Agent action "Coming soon" (2 pages) |
`WorkflowActionType.AI_AGENT` ships, in the picker, no feature flag |
| "There is no built-in if/else logic" (2 pages) |
`WorkflowActionType.IF_ELSE` ships |
| Webhook event filtering "may be added in future releases" (2 pages) |
per-webhook `operations` array with `*.created` / `person.*` / `*.*`
wildcards |
| Many-to-many "coming in H2 2026" | Junction Relations shipped as
public beta; Twenty's own how-to documents it |
| Email campaigns "available soon" (2 pages) | MessageCampaign object,
send/stats jobs, unsubscribe topics all ship |
| CC/BCC "not yet available" | exists on Send Email |
| Workflow retry "on our roadmap" | run-level retry command plus
per-step `retryOnFailure` |
| front-components limitations table | `getBoundingClientRect`,
`offset*`/`client*`/`scroll*`, `getComputedStyle`, `getElementById` all
polyfilled |
| Node SDK "does not exist" | `twenty-client-sdk@2.27.0` ships and is
documented elsewhere in these docs |

Four "coming soon" claims were checked and are **still accurate** —
webhook trigger authentication, dashboard-level filters, dashboard
timezone, background-job priority. Leave them.

One needs rewording rather than promotion: **gauge charts** are
described as on the roadmap, but the upgrade command
`2-3-workspace-command-...-delete-gauge-widgets` says support was
*removed*.

### Priority 3: structural

**30 orphaned pages remain** after the twenty-ui deletion: 15 of 18
`developers/contribute/*`, all 6 `user-guide/getting-started/*`, plus
`self-host.mdx`, `key-rotation.mdx`, `extend.mdx`,
`views-pipelines/overview.mdx`, `ai/capabilities/mcp.mdx`,
`data-migration/how-tos/export-faq.mdx`,
`extend/capabilities/{apis,webhooks}.mdx`. Each needs an explicit
decision: re-add, or delete plus redirect. Two look worth re-adding
rather than deleting — `user-guide/ai/capabilities/mcp.mdx` is accurate,
documents a shipped feature that's a plan line-item, and is reachable
only via a legacy redirect; `views-pipelines/overview.mdx` is linked
from three in-nav pages.
`user-guide/getting-started/capabilities/implementation-services.mdx`
must be merged rather than deleted, since three in-nav pages deep-link
it.

**Duplicate pages.** `getting-started/core-concepts/glossary.mdx` and
`user-guide/getting-started/capabilities/glossary.mdx` are 99%
identical. `developers/extend/webhooks.mdx` and
`developers/extend/capabilities/webhooks.mdx` are 88% identical and
carry the same wrong payload. `workflow-branches.mdx` and
`use-branches-in-workflows.mdx` are both in the sidebar and give
*contradictory* branch-creation instructions.

**Other.** 44 pages have no frontmatter `description`. The Russian
locale is 14 pages behind every other locale, including the entire
document-generator tutorial.

### Still needs a human owner

The legal FAQ promises breach notification "within 48 hours" while
clause 4.6 of the generated DPA (`dpa-template.constant.ts:178`) targets
72. Per direction, the docs keep 48h — a stricter public commitment than
the contract is a deliberate choice — but the DPA and the docs still
disagree, and someone owning the DPA should decide which moves.

Claims about SOC 2, GDPR attestation, backup cadence and AI-training use
could not be substantiated from the repository either way and need the
same treatment.

### Preventing recurrence

Three cheap guards would have caught most of the 75 criticals:

- **A CI check** that every navigation page resolves, every internal
link and image resolves, and no `.mdx` outside `l/` is orphaned. Catches
the entire structural third. This PR leaves the docs in a state where
such a check would pass.
- **Generate the volatile tables from their source enums** — field
types, workflow actions and triggers, filter operands, permission flags,
chart types, env vars — rather than hand-maintaining them. These
accounted for a large share of the major findings.
- **Treat "coming soon" as an expiring assertion**: tag each with the
symbol it depends on and fail the docs build when that symbol appears in
code.

## Suggested order for the rest

1. Workflow variable syntax across the 12 tutorial pages — largest
cluster, mechanical, most directly matches the feedback.
2. Self-host backup/restore/troubleshooting commands — highest blast
radius per reader.
3. Webhook payload and OAuth endpoints — blocks integrators.
4. The nine "coming soon" claims — one line each, and the most visible
form of "docs don't reflect the latest release".
5. Decide the 30 remaining orphans.

## Test plan

- [x] Every internal link and image reference in the docs resolves
- [x] All 171 navigation entries resolve to a file on disk
- [x] No `docs.json` redirect destination is dead
- [x] No inbound links to the deleted `twenty-ui` pages remain
- [x] `docs.json` structure intact after edit (138 redirects, 14
languages)
- [ ] Visual check of the self-hosting SMTP tabs once the docs preview
builds

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23616?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-31 07:13:56 +00:00
github-actions[bot] a5680d1732 chore: sync AI model catalog from models.dev (#23615)
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-31 08:55:36 +02:00
twenty-pr[bot] 510150a016 chore: bump version to 2.27.0 (#23604)
## 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/23604?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-30 20:32:52 +00:00
github-actions[bot] 7a9b32e662 i18n - docs translations (#23608)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-30 20:58:37 +02:00
github-actions[bot] 2d1b976437 i18n - translations (#23603)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23603?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-30 19:02:44 +02:00
github-actions[bot] c17cb49fb9 i18n - docs translations (#23602)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-30 19:02:19 +02:00
Thomas Trompette 53a18d7528 feat(workflow): route all version content readers through the flag-aware sources (#23583)
## What

Follow-up to #23499. Migrates every remaining reader of
`record.trigger`/`record.steps` so all version content flows through the
flag-aware sources, then removes `trigger`/`steps` from the record field
sets. The record CRUD path no longer carries version content anywhere in
the app.

Reading is still entirely behind `IS_WORKFLOW_VERSION_IN_CORE_ENABLED`:
this PR changes who asks, never where the answer comes from. Flag off
remains record reads (via the content hook's record branch), flag on the
core query.

## Per reader

| reader | now reads |
| --- | --- |
| `WorkflowDiagramCanvasEditable` (connect, drag-stop) | flow atom |
| `useDeleteStep` | flow atom |
| `WorkflowEditActionIfElseBody` (branch cleanup) | flow atom |
| `SidePanelWorkflowCreateStepContent` (parent-step lookup) | flow atom
|
| `SidePanelWorkflowStepInfo` | flow atom (explicit instance id),
falling back to `useWorkflowVersionContent` when the visualizer is not
mounted |
| `TestWorkflowSingleRecordCommand` | `useWorkflowVersionContent`;
`ready` gates on content being loaded |
| headless enrichment hook (imperative) | core content query when the
flag is on, record otherwise |
| `WorkflowRunVisualizerEffect` (step output schemas) | the run snapshot
(`state.flow`), which is what a run should show anyway |

## Field-set slimming

`useWorkflowVersion` and `useWorkflowWithCurrentVersion` stop fetching
`trigger`/`steps` (identity fields only). Three call sites lost their
only reason to call `useWorkflowWithCurrentVersion` and were dropped
entirely. Verified by grep that no `currentVersion.trigger/steps` reads
remain; the only remaining `.trigger`/`.steps` accesses are
argument-taking utils whose callers now pass flow/content-sourced
objects.

## Verification

- `nx typecheck twenty-front` green
- Front tests: 1058 green (the enrichment test gained mocks for the
apollo client and flag its hook now uses)
- Full-tree `oxfmt` + `oxlint --type-aware` green (3 remaining warnings
are pre-existing in unrelated record-field files)
- Live click-through pending, flag off and on

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23583?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-30 16:57:11 +00:00
github-actions[bot] 42de0482d0 i18n - translations (#23600)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-30 18:57:50 +02:00
Weiko 0d63906a58 Fix calendar field picker state handling (#23595)
## Context

Changing a calendar date field already updated the record-index calendar
state immediately, so the calendar moved to the new field before the
`updateView` mutation completed.

The options dropdown still derived its selected checkmark and field
labels from `currentView`, which remains unchanged while persistence is
pending. On slower environments this left the previous field name
visible even though the calendar was already using the new field.
Locally the same mismatch existed, but was only visible briefly because
the mutation completed faster.

## What changed

- Read the active start and end date field IDs from the record-index
calendar component state in the calendar options dropdown.
- Use that state for the main options label, the two-field submenu, and
both field-picker selections.
- Use the optimistic end-field state when filtering compatible start
fields and deciding whether an incompatible end field must be cleared.
- Keep the existing view mutation and calendar-state writes unchanged.

## Why

The calendar and its configuration UI now share the same source of truth
while persistence is pending. A field selection updates the calendar,
checkmark, and contextual labels together instead of temporarily mixing
optimistic calendar state with stale persisted view metadata.

## Safety and expected impact

This is frontend state synchronization only. It does not change the
metadata schema, API payloads, or persistence flow. Existing date and
datetime compatibility rules remain in place.

Users should see the selected field name update immediately, including
when the metadata mutation is slow.

## Limitations

This does not change mutation error handling or add rollback behavior.
The calendar atoms were already updated optimistically before this
change, this PR only makes the configuration UI reflect those same
values.

## Validation

- Reproduced the stale selection on qacoco and locally.
- Verified locally that the checkmark moves immediately after selecting
another date field, before the mutation closes the dropdown.
- `npx nx typecheck twenty-front`
- Focused type-aware oxlint on the four changed files, 0 warnings and 0
errors.
- `npx oxfmt --check` on the four changed files.
- `git diff --check`
2026-07-30 16:56:34 +00:00
github-actions[bot] b4102946f4 i18n - translations (#23598)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-30 18:55:01 +02:00
Félix Malfait c25c0f4698 Add Enterprise plan and competitor pricing comparison pages (#23588)
Reworks the pricing surface in one PR: a new Enterprise tier on the
pricing page, five competitor pricing comparison pages, and the pricing
page section that links to them.

## Enterprise tier (pricing page)

- Third plan on both the cloud and self-hosting views: "from $50k /year"
with a "Talk to sales" CTA opening the existing contact modal.
- Cloud bullets: single-tenant isolation, IP allow-listing, SCIM
provisioning, dedicated support & SLA. Self-host: SCIM, air-gapped
deployment, LTS releases.
- Comparison table gains an Enterprise column (inherits Organization
values unless a row overrides) plus an Enterprise category of rows.
- New halftone building icon generated with the /halftone studio to
match the plan icon family.
- Pro and Organization pricing unchanged ($9/$19, in sync with Stripe).

## Competitor comparison pages

-
`/compare-pricing/{hubspot,salesforce,attio,pipedrive,microsoft-dynamics}`:
one shared template driven by a data file per vendor.
- Feature-by-feature cost table (competitor price + unlocking tier +
source link per claim, "checked on July 30, 2026" note, "spotted an
inaccuracy" link), a side-by-side bill for a 20-person team using the
PlanCard visual language with a savings badge cloned from the billing
toggle's -25% chip, and a one-line "fair play" note per vendor.
- Prices are 2026 list prices researched from vendors' public pricing
pages, billed annually. Routes registered in the website route registry
(indexed, sitemap, hreflang).

## Pricing page hub

- The salesfarce section intro becomes "Compare the real cost" with the
five comparison links using the footer's hover-marker link style. The
parody widget is unchanged.

Locale catalogs untouched for the i18n bot. Typecheck, lint, and the
website test suite pass.

Worth a second pair of eyes: the Pipedrive plan names/prices (2025
rebrand; their pricing page blocks fetchers) and the Salesforce "+30% of
spend" support/sandbox figures.
2026-07-30 18:54:30 +02:00
Remi Huigen 830404b215 fix: Decrypt encrypted front component variables (#23494)
## Summary

Fixes #23492

Fixes front-component application variables returning their encrypted
at-rest value instead of their configured plaintext value.

Non-secret application variables (`isSecret: false`) are now decrypted
server-side before being injected into the front-component environment.
Secret variables remain excluded and are never decrypted or exposed to
the browser.

## Root cause

The front-component resolver filtered secret application variables
correctly, but forwarded the cached `encryptedValue` directly. As a
result, `getApplicationVariable()` returned an `enc:v2:...` envelope
rather than the configured value.

## Changes

- Decrypt recognized versioned envelopes for non-secret application
variables.
- Preserve empty and legacy/plain values unchanged for backwards
compatibility.
- Add `SecretEncryptionModule` to the front-component module.
- Add coverage for:
  - decrypting public variables;
  - retaining plaintext compatibility;
  - excluding secret variables without attempting decryption.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23494?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: prastoin <paul@twenty.com>
2026-07-30 16:49:52 +00:00
Thomas Trompette a747e62970 fix(workflow): use as draft with an existing draft and cross-object record page filters (#23524)
Fixes two workflow issues.

### "Use as draft" fails when a draft already exists

`UseAsDraftWorkflowVersionSingleRecordCommand` rendered its own
`OverrideWorkflowDraftConfirmationModal`. Headless commands are
unmounted by `HeadlessEngineCommandWrapperEffect` as soon as `execute`
resolves, so the modal was removed from the tree right after `openModal`
was called and the user saw nothing happen. This regressed when the
command was converted from a rendered `<Command onClick>` to a
self-unmounting effect.

The command now uses `HeadlessConfirmationModalEngineCommandEffect`, the
existing mechanism for headless commands that need a confirmation: it
opens the app-wide `CommandMenuConfirmationModalManager` and keeps the
command mounted until the modal emits its result.
`OverrideWorkflowDraftConfirmationModal`, its modal id and its config
state are deleted.

To keep the "Go to Draft" shortcut, the shared confirmation modal config
gains an optional `linkButton` rendered as a secondary link button that
emits a `cancel` result on click.

The command also resolved the workflow id through `useWorkflowVersion`,
which is `undefined` while the query is in flight, so it threw and
surfaced an error snackbar through the command error boundary. It now
reads the workflow id off the selected record like the sibling workflow
version commands, and waits for the workflow to load before deciding
whether a confirmation is needed.

### `workflow object doesn't have any "workflowId" field` when opening a
workflow from a version

`useRecordShowPagePagination` builds prev/next queries from the parent
view stored in `contextStoreRecordShowParentViewComponentState`.
Navigating from a workflow version record page to its workflow through
the relation chip keeps the workflow version parent view, whose relation
filter compiles to `workflowId: { in: [...] }` and is then sent against
the `workflow` object, which the API rejects.

`useQueryVariablesFromParentView` now ignores the parent view when
`parentViewObjectNameSingular` does not match the current object, which
covers every navigation path between record pages of different objects.

### Test

Added `useQueryVariablesFromParentView.test.tsx` covering both the
matching and mismatching parent view object.

Manually checked on a local instance: override with an existing draft,
"Go to Draft", cancel then re-trigger, and the no-existing-draft path,
plus navigating from a filtered workflow versions view to a workflow.

---------

Co-authored-by: Tom <tom@twenty.com>
2026-07-30 16:45:25 +00:00
martmull a99e62fbca Add application job enqueue limits guard on all message queue drivers (#23570)
## Context

Applications trigger logic functions from several paths (cron, database
events, HTTP routes, install/connect hooks). Without a cap, a single app
can flood the `logic-function-queue` and starve it. This adds enqueue
limits scoped to that queue, mirroring the existing per-application API
rate limiting.

## What changed

`JobEnqueueThrottlerGuard` reuses the `ThrottlerService` token bucket
(same primitive as the API rate limiter) with two tiers:

- **Per application installation**
(`enqueue:throttler:application:{applicationId}`) - lower ceiling,
`APPLICATION_JOB_ENQUEUE_RATE_LIMITING_LIMIT` (default 500).
- **Per application registration**
(`enqueue:throttler:application-registration:{applicationRegistrationId}`)
- higher ceiling shared across all workspaces that installed the same
app, `APPLICATION_REGISTRATION_JOB_ENQUEUE_RATE_LIMITING_LIMIT` (default
2000).

Both share `APPLICATION_JOB_ENQUEUE_RATE_LIMITING_TTL_IN_MS` (default
60s). Both buckets are checked before either is debited, so a rejection
on one tier never burns quota on the other.

**Per-queue guarding.** A `ThrottledMessageQueueDriver` decorator wraps
the concrete driver (BullMQ or Sync) in the `QUEUE_DRIVER` provider and
passes the `queueName` to the guard. The guard only acts on queues in
`GUARDED_ENQUEUE_QUEUES` (currently just `logic-function-queue`); every
other queue is untouched.

**Required application context.** The guard reads a dedicated
`applicationJobEnqueueContextStorage` (AsyncLocalStorage) carrying `{
applicationId, applicationRegistrationId }`, and throws if a
guarded-queue enqueue runs without both. Every logic-function-queue
enqueue site wraps its `add`/`bulkAdd` in
`withApplicationJobEnqueueContext`:

- cron trigger
- database-event trigger (groups logic functions by application, one
batch per application)
- server route trigger
- application post-install hook
- connection-provider on-connect hook

When the limit is reached the guard records a
`JobEnqueueApplicationRateLimited` metric and throws
`ThrottlerException` (mapped to 429 by the existing handlers).

## Files

- `message-queue/guards/job-enqueue-throttler.guard.ts` - the guard
(new)
- `message-queue/storage/application-job-enqueue-context.storage.ts` -
dedicated enqueue context (new)
- `message-queue/constants/guarded-enqueue-queues.constant.ts` -
guarded-queue set (new)
- `message-queue/drivers/throttled-message-queue.driver.ts` - decorator
driver wrapping any driver (new)
- `message-queue/message-queue-core.module.ts` - wires the guard into
the driver provider
- `twenty-config/config-variables.ts` - three tunable `RATE_LIMITING`
config variables
- `metrics/types/metrics-keys.type.ts` -
`JobEnqueueApplicationRateLimited` key
- the 5 enqueue sites above - inject the enqueue context

## Notes / trade-offs

- A logic function whose application has no `applicationRegistrationId`
is skipped at the trigger paths (the install hook throws), matching how
the server route trigger already treats "not linked to a registration".
- `addCron` is left ungated (idempotent upsert).
- Default limits are placeholders and tunable per instance.

## Testing

- `JobEnqueueThrottlerGuard` unit tests (7 cases): non-guarded queue
skip, throw on missing/partial context, two-tier throttling with
distinct limits, per-item token consumption on bulk, no partial debit
when either tier is exhausted.
- Updated `connection-provider-oauth-flow.service.spec.ts` for the new
cache key.
- `npx nx typecheck twenty-server` passes; oxlint + oxfmt clean on
changed files.
2026-07-30 16:40:22 +00:00
BOHEUS cc8fac46c3 Add licensing section to legal FAQ (#23560)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23560?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: Félix Malfait <felix@twenty.com>
2026-07-30 18:36:22 +02:00
Weiko bd65dbd47a Cache metadata lookups during ORM result formatting (#23593)
## Context

Production profiling identified `formatResult` as a recurring CPU
hotspot on read paths, especially for list queries and nested relations.

The formatter receives one metadata snapshot for the complete result,
but previously rebuilt metadata-derived lookup structures for every
record. For each record, including recursively formatted relation
records, it rebuilt or rescanned:

- field name and join-column maps
- composite field property maps
- required composite properties
- date and date-time field collections

This metadata does not change while one result is being formatted, so
the repeated work scaled with the number of records without changing the
output. It also created short-lived allocations that added GC pressure
on busy server pods.

## What changed

- Create a private cache for each top-level `formatResult` invocation.
- Lazily derive formatter metadata once per object metadata ID.
- Reuse it across records in an array and recursively formatted
relations.
- Precompute required composite property names and date-time field
metadata.
- Remove the DATE post-processing pass, which assigned each value back
to itself.
- Keep the exported `formatResult` signature unchanged.

The cache is discarded when the formatting call returns.

## Why use a call-scoped cache

The derived structures are valid for the metadata maps passed to one
`formatResult` call. Keeping the cache local provides reuse for the
complete result batch without adding cross-request state, invalidation
rules, or another long-lived memory cache.

This also preserves existing callers and keeps recursive implementation
details private.

## Safety

- Formatting behavior and returned shapes are unchanged.
- Nested relation formatting still resolves metadata for each target
object type.
- Composite null and default handling, and DATE_TIME validation, are
unchanged.
- Metadata is recomputed for every top-level invocation, so a later
request cannot reuse data derived from an older metadata snapshot.
- No Redis, workspace-cache, database, or public API behavior changes.

## Expected impact

Metadata preparation now scales with the number of object types in a
result instead of the number of records. The largest benefit is expected
for list queries and nested relations, with lower CPU usage and fewer
short-lived allocations.

This is a targeted result-formatting optimization. It does not address
every source of API tail latency or retained cache memory.

## Validation

- Added a nested-relation regression test that verifies unchanged
output.
- The test verifies metadata resolution is bounded per object type
within one invocation and recomputed for a separate invocation.
- Focused formatter Jest suite.
- Existing chart relation-label Jest suite, 10 tests.
- Type-aware Oxlint.
- Oxfmt.
- `yarn nx typecheck twenty-server`.
2026-07-30 16:31:44 +00:00
Paul Rastoin bc0ec6b104 Maintain INDEX view system side effects on deactivated views (#23590)
# Introduction

Follow-up on
https://github.com/twentyhq/twenty/pull/23585#discussion_r3683701472.

Two INDEX view side-effect handlers bailed out when the view had
`isActive: false`. This drops those gates.

# Why

`isActive: false` on a view has exactly one writer: the delete path,
when `isCallerOverridingEntity` is true
(`from-delete-view-input-to-flat-view-or-throw.util.ts`,
`view.service.ts`). It is not in `FLAT_VIEW_EDITABLE_PROPERTIES`, so
nothing else sets it.

So the flag means "the workspace deleted an engine-owned view, and since
the engine owns the row we deactivate instead of hard-deleting". It is a
workspace override of a row we still own, not a signal the row is gone
(that is `deletedAt`). Which makes it precisely the state where the
engine must keep maintaining its own rows: the row still exists, still
belongs to the engine, and is expected to be consistent whenever the
override is lifted.

Skipping the side effect instead left the view permanently incomplete,
with no repair path.

The gates were inherited from the candidate-view scan removed in the
same commit as these handlers were introduced
(`compute-flat-view-fields-from-fields-widgets.util.ts`, #23081). There,
`!view.isActive` filtered which of many views were candidates.
Transplanted into handlers that resolve *the* one deterministic
engine-owned INDEX view identifier, the same predicate stops meaning "is
this a candidate" and starts meaning "silently skip the system side
effect".

# Changes

- `fieldIndexViewFieldOnCreate`: create the INDEX view field even when
the view is deactivated.
- `objectIndexViewLabelIdentifierOnUpdate`: reconcile the label
identifier view field even when the view is deactivated.

`deletedAt` gates are unchanged in both.

The `should noop when the object has no active INDEX view` spec case is
inverted accordingly.

# Follow-up

Both handlers also drop inactive view fields when computing positions,
while `FlatViewFieldValidatorService` builds its `otherFlatViewFields`
with no `isActive` filter. An inactive view field below all active ones
would make a handler emit a label identifier position the validator then
rejects. Unreachable today (view fields are only ever soft-deleted,
never deactivated), so left out of this PR.
2026-07-30 15:42:30 +00:00
github-actions[bot] 550aeafd90 i18n - docs translations (#23589)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-30 17:20:41 +02:00
Paul Rastoin 08891db8be Create INDEX view fields visible on field creation (#23585)
# Introduction

Follow-up on https://github.com/twentyhq/twenty/pull/23081.

Creating a field on an existing object added its column to the object's
index view hidden, while creating the same field alongside its object
added it visible. Same field, different outcome depending on when it was
created.

Both now create it visible. Hiding the column stays one click away, and
that choice is kept as a user override on top of the engine default.

Applies to fields created from now on. Nothing is backfilled: an already
hidden column cannot be told apart from one a user hid on purpose.

`objectSystemFieldsAndIndexViewOnCreate` and the 2-26 reconcile command
are untouched.
2026-07-30 15:16:36 +00:00
github-actions[bot] f3de8ce631 i18n - translations (#23587)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-30 17:17:05 +02:00
Raphaël Bosi 5848c9bd30 Display object, field and view links as chips in the AI chat (#23573)
<img width="3840" height="1876" alt="CleanShot 2026-07-30 at 15 37
46@2x"
src="https://github.com/user-attachments/assets/9fe178b9-c2fa-4b05-9c9d-0cdc80270b67"
/>



https://github.com/user-attachments/assets/34c4e486-c462-4300-ae98-da99f614f069



The AI chat already renders record chips from a `[[record:...]]` marker
the model writes in its prose, but naming an object, field or view
produced plain text. This adds three sibling markers so those render as
chips too, as in the [Figma
design](https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=104416-116261).

- `[[object:<nameSingular>:<label>[[/object]]` links to the record index
page. It is name-keyed rather than id-keyed so an object the assistant
only *proposes* to create still renders as a chip, just without a link.
- `[[field:<id>:<label>[[/field]]` links to the field's settings page,
gated on the `DATA_MODEL` permission.
- `[[view:<id>:<label>[[/view]]` links to the object index page for that
view.

Field and view ids must come from a tool, so an unresolvable one falls
back to plain text rather than a chip that goes nowhere.

The record-only parser becomes one scan over all four kinds. Alternative
order is load-bearing: `[[view:<uuid>:` is shaped exactly like the
legacy prefix-less record marker, so metadata kinds are tried first and
only records keep the legacy `]]` terminator.

Server side is prompt-only. The metadata and view tools return bare
objects rather than `ToolOutput`, so there is nowhere to hang a
structured reference array without wrapping every factory, and the names
and ids the markers need are already in those results verbatim.

Also fixes a pre-existing issue in `LazyMarkdownRenderer`: its
`components` map was rebuilt on every render, and react-markdown uses
each entry as the JSX element type, so every node remounted on every
streamed chunk. Harmless before, expensive once the model is told to
chip every metadata name it writes.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23573?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-30 15:08:21 +00:00
Félix Malfait a9d996ff7e Clarify application licensing and add trademark policy (#23564)
## What

- `twenty-sdk`, `twenty-client-sdk`, `create-twenty-app`,
`twenty-shared` and `twenty-ui` are now MIT (package.json + LICENSE
files). The SDKs are bundled into third-party applications and app front
components import twenty-ui, so these need a permissive license for apps
to be licensable by their authors. `twenty-shared` is included because
both SDKs inline it at build time; an MIT SDK bundling AGPL code would
defeat the purpose. Apps under `packages/twenty-apps` were already MIT.
- Added a "Twenty Application Exception" to LICENSE (additional
permission under AGPLv3 section 7): applications that interact with
Twenty through the app platform interfaces (APIs, manifests, logic
functions, front components, SDKs) are not subject to copyleft and can
be licensed freely by their authors. Modifying Twenty itself remains
fully AGPL, including the network clause.
- Rewrote the LICENSE intro to describe the three licensing zones (AGPL,
Enterprise-marked files, MIT packages) and fixed the intro incorrectly
saying "GPL".
- Added TRADEMARK.md: what anyone can do without asking (self-host,
"built on Twenty", forks under their own name) and what requires
permission (using the name or logo for a product, domain, or hosted
offering).

## Why

Gives app developers and partners legal certainty that building on the
platform does not pull their apps under AGPL, while the core stays AGPL.

The exception and trademark wording should get a legal review before
being announced.
2026-07-30 16:55:19 +02:00
Weiko 3689e89440 Optimize upgrade status gauges with count-only queries (#23574)
## Context

Upgrade health metrics and the admin upgrade-status query currently
share `getInstanceAndAllWorkspacesStatus`.

On a cache hit, that method reads the cached behind/failed workspace
IDs, then hydrates every workspace name with an individual
`CoreEntityCacheService.get` call. This is useful for the admin
response, but the gauges only need the number of workspaces in each
state.

As the number of behind or failed workspaces grows, every gauge refresh
therefore creates a fan-out of entity-cache lookups. Those lookups can
include Redis validation and response deserialization. Production
profiling of slow upgrade-status requests showed
`loadWorkspaceNamesById` and `CoreEntityCacheService.get` on the hot
path, so this PR removes that unnecessary repeated work.

## What changed

- Added a count-only upgrade-status method for metric collection.
- Updated upgrade gauges to use cached ID counts without loading
workspace names.
- Replaced the admin path's per-workspace cache lookups with one
repository query selecting only `id` and `displayName`.
- Removed the upgrade module's now-unused core-entity-cache dependency.

## Why this improves performance

### Metrics path

Before:

- Read the cached upgrade-status IDs.
- Run one entity-cache lookup per behind/failed workspace.
- Discard the hydrated names and only use the array lengths.

After:

- Read the same cached upgrade-status IDs.
- Derive counts directly from those IDs.
- Perform no workspace-name lookup.

**This changes metric collection from a fixed set of status-cache calls
plus `N` entity-cache calls to only the fixed status-cache calls. The
amount of ID data still scales with the number of affected workspaces,
but the Redis/client round-trip fan-out does not.**

### Admin path

The admin response still needs workspace names. It now loads them with
one primary-key `IN` query instead of `N` independent entity-cache
calls. This reduces round trips and repeated cache validation while
preserving the response shape.

## Safety and behavior preservation

- Upgrade-status cache keys, TTLs and invalidation behavior are
unchanged.
- A missing cache marker still triggers the existing full status
refresh.
- Metrics names and values are unchanged.
- The admin GraphQL response is unchanged.
- Cached workspace IDs missing from the database still produce a `null`
name, matching the previous behavior.
- The batched query runs only for callers that request the detailed
admin payload, not for metric collection.

## Expected impact

- Remove recurring per-workspace cache fan-out from every API process
collecting upgrade gauges.
- Reduce Redis client work, response deserialization and event-loop
pressure during metric collection.
- Reduce latency for detailed admin upgrade-status requests.

This targets one profiled source of tail latency. It is not expected to
eliminate all API p99 outliers, which also have independent causes.

## Validation

- 36 focused upgrade-status and gauge tests pass.
- `yarn nx typecheck twenty-server` passes.
- Oxlint passes with zero warnings and errors.
- Oxfmt and `git diff --check` pass.
2026-07-30 14:38:48 +00:00
Weiko ad271ee639 Add connected account handle/provider index (#23580)
## Context

Google messaging webhook notifications resolve connected accounts with
an equality lookup on both `handle` and `provider`:

```ts
connectedAccountRepository.find({
  where: {
    handle: decodedData.emailAddress,
    provider: ConnectedAccountProvider.GOOGLE,
  },
});
```

This lookup runs for incoming Gmail notifications, but
`connectedAccount` currently has no index matching either predicate. As
the table grows, PostgreSQL has to inspect unrelated connected-account
rows for each notification. Under sustained webhook traffic, that adds
avoidable database work and keeps database connections occupied longer.

## What changed

- Add a composite B-tree index on `connectedAccount(handle, provider)`.
- Register the index in the TypeORM entity metadata.
- Add an idempotent 2.26 fast instance command to create the index for
existing installations and remove it on rollback.

The webhook handler and query behavior remain unchanged.

## Why this index

- Both query predicates are equality conditions, so the composite index
supports a targeted lookup.
- `handle` is first because it is the more selective value and also
makes the index useful for handle-prefixed lookups.
- The index is intentionally non-unique. The same provider handle may
legitimately belong to connected accounts in different workspaces, and
this change must not introduce a new data constraint.
- Connected accounts are read by webhooks much more frequently than
their handle or provider changes, so index maintenance overhead should
remain small.

## Expected impact

Webhook account resolution should use an index lookup instead of
scanning the connected-account table. This reduces cumulative PostgreSQL
work and connection occupancy on the Gmail notification path.

This is a targeted database optimization. It should reduce pressure
generated by this high-frequency query, but it is not expected to
resolve every source of API tail latency by itself.

## Safety and rollout

- The instance command uses `CREATE INDEX IF NOT EXISTS` and `DROP INDEX
IF EXISTS`.
- No uniqueness or application behavior changes are introduced.
- Existing rows require no data backfill.
- The index adds bounded storage and write-maintenance overhead.

## Validation

- `yarn nx typecheck twenty-server`
- Type-aware Oxlint on the changed files
- Oxfmt on the changed files
- `git diff --check`


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23580?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-30 14:35:24 +00:00
Weiko fc6a95a37f Throttle local cache expiration sweeps (#23579)
## Context

The workspace cache and core entity cache keep bounded in-process maps.
Entries that have not been read for 30 minutes are removed by an
expiration sweep.

Before this PR, every cache read synchronously walked the entire local
cache, including every stored version, before performing the actual
lookup. The cost therefore grew with the number of cached entries even
when there was nothing to expire. Both caches are used on common server
request paths, so these repeated full-map scans add unnecessary CPU work
and short-lived allocations, which can contribute to event-loop and
garbage-collection pressure under load.

## What changed

- Run each cache's expiration sweep at most once per minute.
- Keep the existing expiration logic unchanged and use one captured
timestamp for the complete sweep.
- Cover both cache services with tests proving that repeated reads
within the interval trigger one sweep and that sweeping resumes after
the interval.

Normal cache reads now pay only for a timestamp check and branch. The
full `O(cache size)` scan runs at most once per minute per process.

## Safety

This does not change cache freshness:

- The 100 ms local freshness window and Redis hash validation still run
as before.
- Explicit cache invalidation is unchanged.
- The 30-minute inactivity threshold is unchanged.
- LRU eviction still runs when entries are inserted.
- Existing local-cache size limits remain unchanged.

An unused entry can remain in memory for at most one additional minute
before the next sweep. This may marginally increase average retained
memory, but it cannot cause unbounded growth or allow stale data to
bypass the existing hash validation.

## Expected impact

This removes a cache-size-dependent operation from a high-frequency
path. The expected benefit is lower CPU and allocation overhead, less
garbage-collection pressure, and improved tail latency when local caches
are populated.

This is intentionally a narrow optimization. It does not claim to
address every source of API tail latency.

## Validation

- `yarn nx typecheck twenty-server`
- Type-aware Oxlint on the changed files
- Oxfmt on the changed files
- Targeted workspace-cache and core-entity-cache Jest suites, 23 tests
passing
2026-07-30 14:24:16 +00:00
martmull e1b5edc07e Compute last contact on relationship changes in last-contact app (#23569)
## What

The `last-contact` app only refreshed the last contact on Companies and
Opportunities when a new email or meeting arrived. When relationships
changed but no interaction happened, those fields went stale:

- Creating an opportunity with an existing point of contact left its
last contact empty.
- Changing an opportunity's point of contact kept the previous contact's
value.
- Assigning a person (who already had contact history) to a company
never surfaced on the company.

This adds logic functions that recompute the derived last-contact fields
when the record or its relationships change.

## Changes

New logic functions (auto-discovered):

- `on-opportunity-created` (`opportunity.created`) and
`on-opportunity-updated` (`opportunity.updated`, `pointOfContactId`)
recompute an opportunity's last contact from its point of contact.
- `on-company-created` (`company.created`) recomputes a company's last
contact from its people.
- `on-person-created` (`person.created`) and `on-person-updated`
(`person.updated`, `companyId`) recompute the former and current
company's last contact when a person joins or leaves.

Shared helpers `recomputeOpportunityLastContact` and
`recomputeCompanyLastContact` mirror the point-of-contact /
most-recent-person value onto the record (clearing it when there is no
contact). Reads use the morph relation subfield (`lastContactItemMessage
{ id }`), matching the existing integration-test read pattern.

The `updatedFields` filters keep these off the interaction write path,
so they never self-trigger.

## Tests

- Unit tests for both recompute helpers and the new logic functions.
- Integration tests covering opportunity-on-create, point-of-contact
change, person joining/leaving a company, and the empty-company case.
- `typecheck`, `lint`, and unit tests pass.

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23569?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-30 14:24:05 +00:00
Paul Rastoin 2be01df271 docs: state v1.23 as prerequisite for cross-version upgrades (#23575)
Fixes #23568

The upgrade guide stated v1.22 was enough before jumping to a 2.x
release. In practice that path fails during the workspace migration with
`column ViewSortEntity.subFieldName does not exist`. Going through v1.23
first works.

Changes in
`packages/twenty-docs/developers/self-host/capabilities/upgrade-guide.mdx`:
- Cross-version upgrade section now says v1.23+ instead of v1.22+,
example updated to v1.23 -> v2.0
- "Before v1.22" section renamed to "Before v1.23" and its instructions
updated

Only the English source is edited, the `l/<locale>/` copies are
Crowdin-managed and will resync.

Co-authored-by: prastoin <paul.rastoin@gmail.com>
2026-07-30 14:22:55 +00:00
martmull 65155fe50c feat(apps): add enqueueJob to run a logic function on the workers (#23527)
Closes twentyhq/core-team-issues#2742

A logic function run is capped by its own `timeoutSeconds` (900s max),
so anything that can't finish in one run — a full re-sync, a per-record
fan-out, a rate-limited third-party API — had no way to continue. This
adds a way to hand that work to the workers.

## What it looks like for an app author

```ts
import { enqueueJob } from 'twenty-sdk/logic-function';

await enqueueJob({
  logicFunctionUniversalIdentifier: '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33',
  payload: { cursor: nextCursor },
  retryLimit: 3,
  priority: 2,
  delayMs: 60_000,
});
```

The target runs in its own process with its own timeout budget. The
classic shape is a function that enqueues *itself* with the next cursor
until there is nothing left.

## Changes

**twenty-shared** — `EnqueueJobInput` / `EnqueueJobOptions` /
`EnqueueJobResult` in `application`.

**twenty-server** — new `application-job` module under
`core-modules/application`, following the `application-key-value`
pattern:
- `enqueueJob` mutation on the metadata API, `@AuthApplication`-scoped
- the lookup is scoped to `applicationId` + `workspaceId` — that's the
authorization boundary, an app can only enqueue its own logic functions,
anything else is `LOGIC_FUNCTION_NOT_FOUND`
- pushes a `LogicFunctionTriggerJob` onto the existing
`logicFunctionQueue`, so the enqueued run goes through the same executor
(and the same execution throttling) as every other trigger
- the queued run inherits the caller's `userId`/`userWorkspaceId`, so
its app access token carries the same permissions as the function that
queued it

**Job options** are range-checked via `ResolverValidationPipe`, since
the values come from application code and an unbounded delay or retry
count would let an app pin work in the shared queue:

| Option | Default | Range |
|--------|---------|-------|
| `retryLimit` | `0` | `0`–`10` |
| `priority` | queue default | `1`–`10` (lower first) |
| `delayMs` | `0` | `0`–7 days |

`retryLimit` defaults to `0` rather than inheriting the server-route
path's `3`: retries re-run the whole handler, so opting in should be the
author's explicit choice.

**twenty-sdk** — `enqueueJob` in `twenty-sdk/logic-function`, same shape
as `runAgent`/`kv`.

**Docs** — new "Background Jobs" page under Extend → Apps → Logic, plus
nav and overview entries.

**Generated** — regenerated `twenty-front/src/generated-metadata` and
`twenty-client-sdk/src/metadata/generated` for the new mutation.

## Tests

- `application-job.service.spec.ts` — 5 unit tests: job options mapping,
defaults, acting-user propagation, application-scoped lookup, not-found
- `enqueue-job.integration-spec.ts` — 5 integration tests: rejects a
non-`APPLICATION_ACCESS` token, enqueues a function the app owns,
rejects a function owned by another application, rejects an unknown
identifier, rejects out-of-range options

All green locally, along with `typecheck` for
`twenty-server`/`twenty-sdk` and oxlint/oxfmt on the touched files.

## Notes for review

- The target is addressed by `universalIdentifier`, matching `runAgent({
agentUniversalIdentifier })` and
`ServerRouteDispatchResult.targetLogicFunctionUniversalIdentifier`.
Addressing by `name` would be friendlier, but logic function names
aren't validated for uniqueness within an app — happy to add it as a
convenience if you'd rather.
- `enqueueJob` returns as soon as the job is accepted; it can't return
the target's result, since the queue driver's `add` returns void.
Documented, with a pointer to the KV store for handing results back.

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23527?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-30 14:20:34 +00:00
Thomas Trompette 6447b7f935 feat(workflow): make the flow atom authoritative for version content, read core behind a flag (#23499)
## What

The frontend half of the workflow-version read switch, plus the small
server query it consumes. Two ideas:

1. **One hook owns where content comes from.**
`useWorkflowVersionContent(workflowVersionId)` returns `{ trigger, steps
}` from the workspace record when `IS_WORKFLOW_VERSION_IN_CORE_ENABLED`
is off (default), and from the new `workflowVersionContent` core query
when on. Switching the source later (core-only, after the column drop)
is a change inside this one hook.
2. **`flowComponentState` becomes authoritative for the builder.** The
canvas, diagram and step output schemas derive from the jotai atom; the
atom is seeded once per version through the hook above; mutations keep
it up to date.

## Why the seeding change is required

Today `WorkflowDiagramEffect` re-seeds the atom from the Apollo record
on **every** `currentVersion` identity change. That has two
consequences:

- Three of the five step/edge hooks (`delete step`, `create edge`,
`delete edge`) never write the atom themselves; they only write the
record and the re-seed papers over it.
- The model breaks the moment content comes from a source mutations do
not write (i.e. core): the stale fetch would be re-applied over every
optimistic edit, and your just-added step would vanish from the canvas.

So the atom is now seeded **once per version**, and
`useUpdateWorkflowVersionCache` applies the mutation's
`stepsDiff`/`triggerDiff` to the atom directly. All five step/edge hooks
get that through their existing call, which closes the three-hook gap in
one move. The step-update, trigger and tidy-up hooks write the atom too.
The record-cache writes are all kept while `trigger`/`steps` still live
on the record (dropped later with the columns).

## The dead wire, now the refresh path

`shouldWorkflowRefetchRequestFamilyState` was set by
`WorkflowSSESubscribeEffect` (reconnect, other-tab create) and
**consumed by nothing**. It is now the external-refresh path: when set,
the builder refetches content and reseeds. Known trade-off: while
connected, another tab's edits no longer live-patch the canvas through
record cache updates (they arrive on reconnect, version switch or
reload). Given concurrent editing of one draft has no conflict handling
anyway, that seemed acceptable; easy to extend the SSE effect to set the
flag on update events if we want live propagation back.

## Untouched by design

- **Run visualizer**: feeds the same atom from the immutable
`workflowRun.state.flow` snapshot; that duality (version content or run
snapshot) is exactly why the atom stays separate from the record store.
- **Version visualizer** (read-only): reseeds on content change, safe
because nothing writes its instance optimistically.
- Peripheral readers of `currentVersion.trigger/steps` (test-workflow
command, headless command enrichment, if-else body, etc.) still read the
record. Correct while dual-writing continues; they move to the content
hook before workspace content writes stop (tracked in the migration
plan).

## Verification

- `nx typecheck` green on both packages; `oxfmt` + `oxlint --type-aware`
green on all 16 changed files
- Front unit tests: 134 suites / 993 tests green (the two hook tests
gained the visualizer instance context their hooks now require)
- New server integration test for `workflowVersionContent`
- **Live click-through pending**: step create/delete/duplicate, edge
create/delete, trigger edit, tidy-up, draft create/discard, activation,
version viewer, run viewer, with the flag off and on. The failure mode
this PR guards against (an edit vanishing from the canvas) does not show
up in typecheck or unit tests.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23499?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-30 14:00:10 +00:00
Félix Malfait c25ae72914 Delete PRODUCT.md (#23577) 2026-07-30 15:30:20 +02:00
Félix Malfait 2d74eca41c Delete DESIGN.md (#23576) 2026-07-30 15:29:50 +02:00
github-actions[bot] 5977185c34 i18n - translations (#23572)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23572?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-30 15:04:14 +02:00
Paul Rastoin 5adc3ab4a2 Pin last-contact to twenty &gt;=2.26.0 (#23520)
Follow-up to #23081.

`twenty-last-contact@1.1.3` stopped declaring its INDEX view fields
explicitly and now relies on the engine's `fieldIndexViewFieldOnCreate`
to provision the INDEX view column of each app field. That handler only
exists from `2.26`, but the app still advertised `engines.twenty:
">=2.23.0"`.

This bumps the range to `>=2.26.0` and documents it in the changelog.

## Why the range matters

`engines.twenty` is checked in two different places against two
different versions:

- `ApplicationTarballService.extractAndValidateTarball` →
`validateServerCompatibility`, against the **store server's** inferred
version. So `1.1.3` can only be deployed once the store instance is on
`2.26`.
- `ApplicationInstallService.runInstall` →
`validateWorkspaceCompatibility`, against the **workspace's completed
upgrade version**.

The second one is the reason for this PR. Publishing a new version calls
`enqueueAutoUpgradeApplications`, and the auto-upgrade path
(`ApplicationUpgradeService.upgradeApplicationToVersion`) does not pass
`skipWorkspaceCompatibilityCheck`. Without the bump, a workspace that
has not yet completed the `2.26` workspace commands would be
auto-upgraded to `1.1.3` and end up with no last-contact columns at all:
the manifest no longer declares them, and pre-`2.26` there is no handler
to provision them. With `>=2.26.0` those workspaces are skipped and stay
on `1.1.2`, which still works on a `2.25` server.

## No SDK bump needed

The only SDK surface the deleted `src/view-fields/*` files used was
`STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.<object>.views.*.universalIdentifier`,
which is exactly what #23081 mutated. Everything left in the app reads
only `.<object>.universalIdentifier`, unchanged, so
`twenty-sdk@2.23.0-alpha.2` still transpiles `1.1.3` to the manifest
`2.26` expects.

## Note on the version number

This pins the existing `1.1.3`, which assumes it has not been deployed
to the store yet. If it has, `validateVersionProgression` rejects a
same-version deploy and this needs to go out as `1.1.4` instead — a
one-line change on this branch.

## Not covered here

Stale `1.1.2` installs on a `2.26` server keep working at runtime (view
fields resolve by database id) but fail any manifest re-sync with `View
not found`, since the standard INDEX view identifiers they target were
renamed by `upgrade:2-26:reconcile-index-view-universal-identifier`.
They will be picked up by auto-upgrade once `1.1.3` is published.
Force-upgrading them from within the `2.26` workspace upgrade, as done
for people-data-labs in `2.23`, would need
`skipWorkspaceCompatibilityCheck: true` (the command runs before the
workspace is marked as having completed `2.26`) and is left out of this
PR.


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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23520?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-30 15:04:08 +02:00
Raphaël Bosi a3beea893d Revert the external link confirmation popup for front components (#23567)
Reverts #23270 and #23404.

Links in front components navigate natively again, with no confirmation
popup and no per-app trusted-origins state in localStorage.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23567?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-30 12:55:55 +00:00
nitin 66e7093524 Use client SDK /s dispatch for call-recorder own-route posts (#23558)
Migrates call-recorder's own-route self-invoke helper off hand-resolved
`TWENTY_FUNCTIONS_URL` and onto the client SDK's built-in `/s` dispatch
(#22863): `postToOwnRoute` now constructs `RestApiClient` with no base
URL and posts to `/s${path}`, letting the SDK resolve
`TWENTY_FUNCTIONS_URL` itself and fall back to `${TWENTY_API_URL}/s`
when it is empty (works on bare multiworkspace hosts since #23490).
Removes the now-unused `resolveOwnRouteBaseUrl` util, its test, and the
env-var-name constant.

Where `TWENTY_FUNCTIONS_URL` is injected non-empty the SDK builds the
identical URL; where it is empty the old code threw and returned false,
while the SDK fallback works on servers with #23490 and fails-caught
identically on servers without it.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23558?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-30 12:40:43 +00:00
Weiko e4e1d24731 Prevent overlapping workspace cleanup executions (#23522)
## Context

The suspended-workspace cleanup is a long-running scheduled job. Under
database or cache pressure, BullMQ can consider an execution stalled and
start a replacement on another worker while the original execution is
still running.

Both executions can then enumerate the same suspended workspaces and run
destructive cleanup concurrently. This amplifies the initial slowdown:

1. Multiple cleanup transactions target the same workspace data.
2. Transactions wait on each other's locks.
3. Database connections remain occupied while waiting.
4. Other workers and API requests have fewer connections available.

There is a second source of unnecessary lock duration in workspace
deletion. The deletion transaction currently starts before field
metadata is read from the workspace cache. If that lookup is slow, the
transaction stays open during an unrelated cache wait.

## What changed

### Prevent overlapping scheduled cleanups

- Acquire a non-blocking PostgreSQL advisory lock before listing
suspended workspaces.
- Skip the execution when another worker already holds the lock.
- Keep the lock on one dedicated PostgreSQL session for the full
callback.
- Release the lock in all normal and error paths.
- Discard the database connection if lock acquisition or release has an
ambiguous failure, preventing a session that may still own the lock from
returning to the pool.
- Encapsulate this lifecycle in `PostgresAdvisoryLockService`, exported
by `TypeORMModule`, so other coarse-grained jobs can reuse it without
handling acquisition and release themselves.

### Shorten the workspace deletion transaction

- Read field metadata and build deletion chunks before starting the
transaction.
- Pass the precomputed chunks into the transactional deletion loop.
- Keep the existing deletion order and SQL behavior unchanged.

## Why a PostgreSQL advisory lock

The lock needs to coordinate workers running in different pods. A
PostgreSQL session advisory lock provides the required behavior:

- It is shared across all workers using the same database.
- Acquisition is non-blocking, a duplicate execution can exit
immediately.
- It has no TTL or renewal heartbeat that could expire during the same
event-loop stall that caused BullMQ to recover the job.
- PostgreSQL automatically releases it when the owning session or
process disappears.

This is deliberately scoped to `CleanSuspendedWorkspacesJob`. It
prevents overlapping scheduled executions, but it is not an exactly-once
mechanism or a global mutex around every workspace-deletion entry point.

## Expected impact

- Prevent one slow cleanup execution from becoming several concurrent
cleanup executions.
- Reduce database lock contention and connection-pool pressure during
cleanup.
- Avoid holding deletion transaction locks while waiting for
workspace-cache data.
- Reduce cleanup-related API latency bursts without changing normal
cleanup semantics.

The advisory lock holds one core database connection for the duration of
the scheduled cleanup. This is intentional and bounded to the single
lock owner.

## Validation

- Focused advisory-lock tests cover successful execution, contention,
callback failure, and unsafe connection disposal when unlock fails.
- Cleanup-job tests cover both the lock-owner and skipped-execution
paths.
- Workspace-service coverage verifies that field metadata is loaded
before the deletion transaction starts.
- `yarn nx typecheck twenty-server`
- Oxlint, Prettier, and Oxfmt checks on the changed files
2026-07-30 12:34:27 +00:00
Paul Rastoin dbd2eac69c Let the instance upgrade version reach releases without instance commands (#23552)
Fixes the CI failure on #23520:

An upgrade version sequence has to at least contain one instance or one
workspace command
Workspaces commands do not run for the instance level and aren't
triggered automatically
Explaining this PR need

<img width="1396" height="954" alt="image"
src="https://github.com/user-attachments/assets/5455d05b-b286-482a-8914-808f55f3b0bf"
/>


```
Upload failed: App requires Twenty server >=2.26.0 but this server is 2.25.0.
```

The server really is 2.26 (`TWENTY_CURRENT_VERSION = '2.26.0'`), but
`validateServerCompatibility` resolves the instance version through
`UpgradeMigrationService.getInferredVersion()`, which reads the last row
in `core.upgradeMigration` with `workspaceId IS NULL AND isInitial =
false` and takes the version prefix off its name. Instance commands are
the only ones that write a `workspaceId`-null row, and `2-26/` ships
none (only three workspace commands), so the highest instance command in
the tree is still
`2-25-instance-command-fast-1785230296000-add-is-hidden-to-agent-message`.
A fully migrated 2.26 server infers 2.25.0, and any app declaring
`engines.twenty: ">=2.26.0"` is unpublishable.

Two defects, both of which `getWorkspaceCompletedVersion` already
avoids:

- **Not sequence-aware.** The workspace path walks the registered
sequence and only credits a version once the cursor sits on that
version's last step. The instance path just reads the cursor's prefix,
so a version contributing zero instance commands is unreachable.
- **Not status-aware.** `getLastAttemptedInstanceCommand` filters on
`attempt = MAX(attempt)` but not on status, so a *failed* 2.25 command
still made the server report 2.25.0.

## What changed

`UpgradeStatusService` gains `getInstanceCompletedVersion()`, the
instance-scope mirror of `getWorkspaceCompletedVersion`. It walks the
sequence filtered to instance steps, requires the cursor to sit on the
last instance step of its version *and* be `completed`, then advances
through any later supported version that declares no instance command at
all.

The version-skipping rule is the part that unblocks 2.26: a release with
no instance-level work has nothing for the cursor to land on, so it is
reached as soon as the last version that does have instance commands is
done. A version whose instance command exists but has not run still
holds the cursor back.

- `validateServerCompatibility` calls the new method;
`UpgradeMigrationService` is no longer a dependency of
`ApplicationVersionValidationService`.
- `getInstanceStatus` reports it as `inferredVersion`, so the upgrade
gauge metric and `upgrade:status` CLI stop showing 2.25.0 on a 2.26
server.
- `getInferredVersion` is deleted. Its one remaining caller passed a
command name, which is just `extractVersionFromCommandName`.
- Cursor resolution is extracted to
`resolve-completed-version-from-cursor.util`, now shared by both scopes;
the skip rule lives in
`advance-through-versions-without-instance-commands.util`.

The asymmetry between the two scopes is intentional and stays: instance
commands record a row per workspace as well, so workspace cursors land
on both command kinds and never had this gap.

## Testing

- `npx nx typecheck twenty-server` clean, `npx nx lint:diff-with-main
twenty-server` clean.
- 294 unit tests pass across the upgrade and application modules,
including 7 new ones for `getInstanceCompletedVersion`. Two pin the
boundary: a trailing workspace-only version is reached, a trailing
version whose instance command has not run is not.
- The fixture in `upgrade-status.service.spec.ts` used
`1.21.0`/`1.22.0`/`1.23.0`, which are real entries in
`TWENTY_PREVIOUS_VERSIONS`. With the skip rule in place that sequence
read as "every version from 2.0 onward has no instance commands" and
walked to the end, so the fixture is renumbered to `0.2x.0` to keep
those tests on cursor resolution alone.
- `failing-app-installation-workspace-version.integration-spec.ts`
already carried a comment describing this bug as a hazard it worked
around. The workaround still holds, but integration tests were not run
here (no DB in this session) — the stale comment is updated.

#23520 stays at `>=2.26.0` and unblocks once this lands.

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23552?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-30 12:32:38 +00:00
Raphaël Bosi 0ff9e77fd3 Hide app record-selection commands when all records are selected (#23561)
PDL enrichment and call-recorder's call summary are `RECORD_SELECTION`
commands backed by headless front components, which only receive record
ids in selection mode. Under select all the context store switches to
exclusion mode, so they received an empty `recordIds` array and still
reported success while enriching nothing.

They now declare `conditionalAvailabilityExpression: !isSelectAll`, so
they disappear from the command menu and quick actions while select all
is active. Both app versions are bumped since the server rejects
redeploying an equal version.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23561?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-30 12:30:10 +00:00
Paul Rastoin eb69e56441 [Upgrade] Add a detached-run wrapper for the upgrade command (#23497)
Long upgrades are started over `kubectl exec` into the command-runner
pod, where the process is a child of the exec'd shell: a dropped SSM
tunnel, a closed laptop or a dead VPN kills the run mid-way. There is no
tmux in the image (Alpine, `sh: tmux: not found`).

`scripts/upgrade-background.sh` puts the run in its own session with no
controlling terminal and streams its output from a log file, so losing
the connection detaches the stream instead of killing the upgrade.

Builds on #23481. The cooperative shutdown path is untouched, this is
tooling around it. No TypeScript changed.

## Commands

```bash
yarn upgrade:background [args]   # start detached, then stream the log
yarn upgrade:background:logs     # re-attach from another shell
yarn upgrade:background:stop     # graceful stop; --now immediate, --force SIGKILL
```

`[args]` is forwarded verbatim to `upgrade`, so it takes that command's
options and no others:

| Option | Effect |
| --- | --- |
| `-d`, `--dry-run` | simulate without making changes |
| `-v`, `--verbose` | verbose output |
| `-w`, `--workspace-id <id>` | restrict to a workspace, repeatable; all
provisioned workspaces if omitted |
| `--start-from-workspace-id <id>` | resume from a workspace, ascending
id order |
| `--workspace-count-limit <n>` | process at most n workspaces,
ascending id order |

`-w` and `--start-from-workspace-id` are mutually exclusive, `upgrade`
rejects the combination.

## Example

```ts
➜  twenty-server git:(claude/upgrade-detached-run-wrapper-5nkb0v) ✗ yarn upgrade:background
Running (pid 15779), logging to /tmp/twenty-upgrade.log
Ctrl+C detaches the stream only. Use 'yarn upgrade:background:stop' to stop the run.
^C%
➜  twenty-server git:(claude/upgrade-detached-run-wrapper-5nkb0v) ✗ yarn upgrade:background:stop
SIGTERM sent to 15779, it finishes the step in progress then stops (exit 143)
Tail of /tmp/twenty-upgrade.log:
[Nest] 15779  - 07/30/2026, 10:47:50 AM     LOG [UpgradeAwareEntityMetadataAdapter] [upgrade-metadata] hidden columns on FieldMetadataEntity: standardOverrides,isCustom
[Nest] 15779  - 07/30/2026, 10:47:50 AM     LOG [UpgradeAwareEntityMetadataAdapter] [upgrade-metadata] applied cursor=217 renamed=0 unavailable=0 hiddenColumns=5
[Nest] 15779  - 07/30/2026, 10:47:50 AM     LOG [UpgradeAwareEntityMetadataAdapter] [upgrade-metadata] hidden columns on RolePermissionFlagEntity: flag
[Nest] 15779  - 07/30/2026, 10:47:50 AM     LOG [UpgradeAwareEntityMetadataAdapter] [upgrade-metadata] hidden columns on ObjectMetadataEntity: standardOverrides,isCustom
[Nest] 15779  - 07/30/2026, 10:47:50 AM     LOG [UpgradeAwareEntityMetadataAdapter] [upgrade-metadata] hidden columns on FieldMetadataEntity: standardOverrides,isCustom
[Nest] 15779  - 07/30/2026, 10:47:50 AM     LOG [UpgradeAwareEntityMetadataAdapter] [upgrade-metadata] applied cursor=207 renamed=0 unavailable=0 hiddenColumns=5
[Nest] 15779  - 07/30/2026, 10:47:50 AM     LOG [DatabaseConfigDriver] [INIT] Loading initial config variables from database
[Nest] 15779  - 07/30/2026, 10:47:50 AM     LOG [DatabaseConfigDriver] [INIT] Config variables loaded: 1 values found in DB, 104 falling to env vars/defaults
[Nest] 15779  - 07/30/2026, 10:47:50 AM     LOG [UpgradeCommand] Initialized upgrade sequence: 217 step(s)
[upgrade] event=sequence.initialized stepCount=217 dryRun=false
[Nest] 15779  - 07/30/2026, 10:47:50 AM     LOG [WorkspaceIteratorService] Running on workspace 20202020-1c25-4d02-bf25-6aeccf7ea419 1/2
[Nest] 15779  - 07/30/2026, 10:47:50 AM     LOG [WorkspaceCommandRunnerService] Upgrading workspace 20202020-1c25-4d02-bf25-6aeccf7ea419 1/2
[upgrade] event=workspace.start workspaceId=20202020-1c25-4d02-bf25-6aeccf7ea419 index=1 total=2 dryRun=false
[Nest] 15779  - 07/30/2026, 10:47:50 AM     LOG [WorkspaceCommandRunnerService] Upgrade for workspace 20202020-1c25-4d02-bf25-6aeccf7ea419 completed.
[upgrade] event=workspace.success workspaceId=20202020-1c25-4d02-bf25-6aeccf7ea419 executedByVersion=unknown dryRun=false
[Nest] 15779  - 07/30/2026, 10:47:50 AM     LOG [WorkspaceIteratorService] Running on workspace 3b8e6458-5fc1-4e63-8563-008ccddaa6db 2/2
[Nest] 15779  - 07/30/2026, 10:47:50 AM     LOG [WorkspaceCommandRunnerService] Upgrading workspace 3b8e6458-5fc1-4e63-8563-008ccddaa6db 2/2
[upgrade] event=workspace.start workspaceId=3b8e6458-5fc1-4e63-8563-008ccddaa6db index=2 total=2 dryRun=false
[Nest] 15779  - 07/30/2026, 10:47:50 AM     LOG [DummySleepCommand] Sleeping for 30000ms on workspace 3b8e6458-5fc1-4e63-8563-008ccddaa6db
[Nest] 15779  - 07/30/2026, 10:47:53 AM    WARN [CommandShutdownService] Received SIGTERM, finishing the step in progress then stopping. Send SIGTERM again to exit immediately.
Follow with 'yarn upgrade:background:logs'. Still stuck: 'stop --now'. Last resort: 'stop --force'.
➜  twenty-server git:(claude/upgrade-detached-run-wrapper-5nkb0v) ✗ yarn upgrade:background:stop --now
Second SIGTERM sent to 15779, immediate exit with the step in progress left unfinished
Tail of /tmp/twenty-upgrade.log:
[Nest] 15779  - 07/30/2026, 10:47:50 AM     LOG [UpgradeAwareEntityMetadataAdapter] [upgrade-metadata] hidden columns on RolePermissionFlagEntity: flag
[Nest] 15779  - 07/30/2026, 10:47:50 AM     LOG [UpgradeAwareEntityMetadataAdapter] [upgrade-metadata] hidden columns on ObjectMetadataEntity: standardOverrides,isCustom
[Nest] 15779  - 07/30/2026, 10:47:50 AM     LOG [UpgradeAwareEntityMetadataAdapter] [upgrade-metadata] hidden columns on FieldMetadataEntity: standardOverrides,isCustom
[Nest] 15779  - 07/30/2026, 10:47:50 AM     LOG [UpgradeAwareEntityMetadataAdapter] [upgrade-metadata] applied cursor=207 renamed=0 unavailable=0 hiddenColumns=5
[Nest] 15779  - 07/30/2026, 10:47:50 AM     LOG [DatabaseConfigDriver] [INIT] Loading initial config variables from database
[Nest] 15779  - 07/30/2026, 10:47:50 AM     LOG [DatabaseConfigDriver] [INIT] Config variables loaded: 1 values found in DB, 104 falling to env vars/defaults
[Nest] 15779  - 07/30/2026, 10:47:50 AM     LOG [UpgradeCommand] Initialized upgrade sequence: 217 step(s)
[upgrade] event=sequence.initialized stepCount=217 dryRun=false
[Nest] 15779  - 07/30/2026, 10:47:50 AM     LOG [WorkspaceIteratorService] Running on workspace 20202020-1c25-4d02-bf25-6aeccf7ea419 1/2
[Nest] 15779  - 07/30/2026, 10:47:50 AM     LOG [WorkspaceCommandRunnerService] Upgrading workspace 20202020-1c25-4d02-bf25-6aeccf7ea419 1/2
[upgrade] event=workspace.start workspaceId=20202020-1c25-4d02-bf25-6aeccf7ea419 index=1 total=2 dryRun=false
[Nest] 15779  - 07/30/2026, 10:47:50 AM     LOG [WorkspaceCommandRunnerService] Upgrade for workspace 20202020-1c25-4d02-bf25-6aeccf7ea419 completed.
[upgrade] event=workspace.success workspaceId=20202020-1c25-4d02-bf25-6aeccf7ea419 executedByVersion=unknown dryRun=false
[Nest] 15779  - 07/30/2026, 10:47:50 AM     LOG [WorkspaceIteratorService] Running on workspace 3b8e6458-5fc1-4e63-8563-008ccddaa6db 2/2
[Nest] 15779  - 07/30/2026, 10:47:50 AM     LOG [WorkspaceCommandRunnerService] Upgrading workspace 3b8e6458-5fc1-4e63-8563-008ccddaa6db 2/2
[upgrade] event=workspace.start workspaceId=3b8e6458-5fc1-4e63-8563-008ccddaa6db index=2 total=2 dryRun=false
[Nest] 15779  - 07/30/2026, 10:47:50 AM     LOG [DummySleepCommand] Sleeping for 30000ms on workspace 3b8e6458-5fc1-4e63-8563-008ccddaa6db
[Nest] 15779  - 07/30/2026, 10:47:53 AM    WARN [CommandShutdownService] Received SIGTERM, finishing the step in progress then stopping. Send SIGTERM again to exit immediately.
[Nest] 15779  - 07/30/2026, 10:48:00 AM    WARN [CommandShutdownService] Received SIGTERM again, exiting immediately. The step in progress is left unfinished, rerun the command to resume from the last recorded step.
EXIT=143
```

## Not a concurrency guard

`start` refuses when it can see a live run, but that only keeps this
wrapper's own bookkeeping straight, one PID file and one log per run.
The PID file is in the container's `/tmp`, so a second pod or a laptop
pointed at the same database sees none of it.

Nothing in `upgrade` prevents two sequences either: `upgradeMigration`
records only `completed` and `failed`, so it has no in-progress state to
lock against, and the sequence runner takes no advisory lock. Out of
scope here, and worth a follow-up if we want it enforced rather than
operational.

## Dockerfile

`scripts/` was not copied into the server image, so all three commands
would have failed with ENOENT in the pod. Added the COPY, plus a `chmod
+x` matching the one already on `entrypoint.sh`.

Rest is documented in `docs/UPGRADE_COMMANDS.md`, including the
exit-code table and why a graceful stop is always safe to rerun.
2026-07-30 12:24:07 +00:00
github-actions[bot] d5d0726216 i18n - translations (#23563)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23563?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-30 14:14:49 +02:00
Raphaël Bosi 38ad13655c Auto-start the workspace setup chat with a data model proposal (#23437)
https://github.com/user-attachments/assets/d447372b-c1b4-4c95-bac9-a8f8efa7d4a1



When the workspace creator lands on `/workspace-setup` after onboarding,
the AI chat now starts on its own: an invisible first message, built
server-side from the company enrichment collected in #23199, asks the
assistant to propose a data model tailored to the business. The proposal
streams in; the user never sees the prompt.

- New `startWorkspaceSetupChat` mutation: creator only, gated on
`IS_ONBOARDING_AI_CHAT_ENABLED`, available models and credits.
Idempotent per user and workspace via a `keyValuePair` pointing at the
thread, so a reload or a second tab joins the same conversation instead
of starting a new one.
- The thread holds exactly one hidden `USER` message combining the
company context and the setup instructions, which keeps the
one-hidden-message-per-thread index from #23199 satisfied. It goes
through a dedicated streaming path that never queues, so the prompt
cannot resurface as a visible message.
- The assistant only proposes. It creates nothing until the user
approves, then builds the model with the `metadata-building` skill.
Objects and fields get English names with labels in the user's language,
and the conversation continues in that language.
- With no enrichment (consumer email domain, or the integration
disabled) the kickoff still runs, and the assistant asks one short
question about the business before proposing.
- `findLatestSentUserMessage` no longer filters out hidden messages, so
a failed kickoff turn stays retryable, and the no-message chat error
surface now offers retry for stream errors.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23437?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-30 12:06:13 +00:00