Commit Graph

332 Commits

Author SHA1 Message Date
Abdul Rahman 0bf4b53af3 feat(slack): cache the Slack bot user id at connect time (#23726)
## What

Caches the Slack bot user id in workspace kv so the channel welcome
stops calling `auth.test` on every channel-join event.

## Why

Slack fires `member_joined_channel` for **every** person joining **any**
channel the bot sits in, not just for the bot itself. The welcome path
had to answer "was that our bot?", and did it by resolving the Slack
connection and calling `auth.test` — a connection lookup plus an
external API call, on every event, to conclude "no, that was a human, do
nothing".

The bot user id never changes for a given connection, so asking Slack
repeatedly is the wrong shape.

## How

`registerSlackConnection` already calls `auth.test` in the `onConnect`
hook and had `user_id` in hand, so it now writes it to workspace kv.
`resolveSlackBotUserId` reads it, falling back to `auth.test` (and
backfilling) for connections created before this change.

Reconnecting is the only thing that can change the bot user id, and
reconnecting re-runs that hook — so the cache is self-correcting and
needs no TTL.

Because resolving the id no longer needs a Slack client, the bot check
moved ahead of the connection lookup:

| per join event | before | after |
|---|---|---|
| Twenty round trips | 1 | 1 |
| Slack API calls | 1 | 0 |

A secondary win: previously `getSlackClient()` ran before the bot check
and threw on failure, so a revoked Slack connection made **every
unrelated human join** fail its job and retry. Now a human join answers
from kv and returns cleanly; the connection is only touched when there
is genuinely something to post.

## Claim ordering

Moving the client lookup after the claim opened a window where the claim
is held but nothing was posted, so that path now releases the claim
before throwing. The invariant the file follows is unchanged: release on
any failure that produced no message, keep it once a message is out (a
retry must not repost the channel message).

## Renames

`claimSlackTeam` → `registerSlackConnection`, and the logic function
`slack-team-claim` → `slack-register-connection`, since it now does more
than claim the team and connect-time work will keep landing there.

**The universal identifier value is unchanged** (`a29ae15d-…`) — it is
the app's stable identity and what the connection provider binds
`onConnectLogicFunction` to. Only the constant's name moved. Worth a
second pair of eyes in review, since that is exactly the kind of thing a
rename sweep regenerates by reflex.

Note this changes `name` and `sourceHandlerPath`/`builtHandlerPath` in
the manifest. Both are updates keyed on the unchanged identifier, not a
delete-and-recreate, so installed apps re-sync cleanly — but the app
needs rebuilding so the bundle path matches.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23722?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. -->



## Cache correctness

A wrong cached id fails silently — the bot's own join reads as someone
else's and the welcome never fires — so the entry is bounded and
self-healing in three ways:

- **Failed write drops the key.** Leaving the previous id in place would
keep a superseded value authoritative. An absent cache is rebuilt from
`auth.test`; a wrong one is believed.
- **Entries expire after 7 days.** `registerSlackConnection` rewrites on
every connect, so the expiry only matters when that write never lands.
- **A kv outage falls through to `auth.test`** rather than throwing,
which keeps the human joins that make up nearly all these events from
failing their job.
2026-08-06 09:50:53 +00:00
nitin 4137a1f9fc Stop classifying Recall bot-detection timeouts as NOT_RECORDED in call-recorder (#23812)
Removes the two `timeout_exceeded_only_bots_detected_*` sub codes from
`NOT_RECORDED_RECALL_SUB_CODES`, so a bot-detection leave is handled
like any other call ending (`call_ended` -> PROCESSING -> artifact
import -> COMPLETED).

These sub codes are leave reasons, not capture verdicts. Bot detection
only fires when participants are present (otherwise `noone_joined` fires
first), and any participant starts the recording, so a bot-detection
ending virtually always has a real recording behind it. It is also the
app's own configured exit path whenever a third-party notetaker
(Fireflies, Otter) lingers after the humans leave, since a lingering bot
keeps `everyone_left_timeout` from ever firing. Classifying it as
NOT_RECORDED stamped successfully recorded calls as failures and skipped
artifact import; bots in the production Recall workspace end with this
sub code near-daily.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23812?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-08-05 13:54:42 +00:00
nitin f893b214e2 Make the call recorder transcript provider an application variable (#23789)
## What

Post-call transcription was locked to Gladia by a frozen constant
(#23532). It is now a `CALL_RECORDER_TRANSCRIPT_PROVIDER` application
variable that a workspace admin picks in app settings.

## Changes

- **Recall.ai transcription (`recallai_async`) is the default.** It is
the only provider that needs no third-party key in the Recall dashboard,
so a fresh install transcribes without extra setup. Gladia
(`gladia_v2_async`) stays available with code switching for
mixed-language calls, and still requires a Gladia API key in the Recall
dashboard per region.
- Providers name their language options differently (`recallai_async`
takes `language_code`, `gladia_v2_async` takes
`language_config.code_switching`), so the variable holds a Recall
provider id and the app owns the per-provider payload in
`RECALL_ASYNC_TRANSCRIPT_PROVIDERS`. Unset or unrecognized values fall
back to the default, matching how `getBotImageBackground` and
`isCallRecordingSummaryEnabled` read their variables.
- `SETUP.md` now frames the Gladia key as conditional on that selection
rather than a hard requirement, and lists the new variable alongside the
other application variables.
- App bumped to 1.7.0. No new server capability is needed, so
`engines.twenty` stays at `>=2.26.0`.

## Upgrade note

Existing installs run on Gladia today through the old constant, and move
to Recall.ai transcription on upgrade unless the variable is set.
Workspaces that rely on code switching for mixed-language calls should
select Gladia after deploying.

## Tests

533 unit tests pass, typecheck and lint clean.

- `recall-bot-api.test.ts` asserts the `create_transcript` request body
for both the default and a Gladia selection.
- `get-recall-async-transcript-provider.test.ts` covers the default, the
fallback for an unsupported provider, and a drift guard asserting the
manifest's SELECT options match the keys of the provider map, so adding
a provider to one without the other fails.



<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23789?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-08-05 11:51:44 +00:00
Abdul Rahman 42aa566e32 feat(twenty-slack): link records and format the assistant reply footer (#23539)
Follow-up to the Slack bot branch, improving how assistant replies read
in Slack.

## Problem

The assistant never had the workspace URL. `buildSlackAssistantPrompt`
injected the request, requester and thread context, and the agent prompt
said nothing about links, so a record it created or found came back as
plain text with no way to open it. The reply was also a single
`markdown_text` blob with `_Answered in 3s_` appended to the answer.

## Changes

**Record deep links.** `fetchWorkspaceBaseUrl` resolves the workspace
URL from `currentWorkspace { workspaceUrls }`, preferring a custom
domain over the subdomain. It runs in parallel with the existing Slack
context fetch, so no extra latency. The prompt carries the base URL plus
the `[Record Name](base/object/<objectNameSingular>/<recordId>)` rule.
When the URL cannot be resolved the prompt explicitly forbids writing
any Twenty URL, so a failed lookup degrades to plain record names rather
than invented links.

**Reply structure.** The answer now goes out as Block Kit: a `markdown`
block for the body and a `context` block for the duration, so it reads
as a footer rather than italic text tacked onto the answer.
`getSlackChatMessageBodyFields` grew a blocks variant that keeps the
message text as Slack's notification and screen-reader fallback, and
`slackUpdateMessageHandler` now falls back to plain text on
`invalid_blocks` for blocks as well as markdown.

## Screenshots

### Before
<img width="344" height="161" alt="Screenshot 2026-07-30 at 8 13 55 AM"
src="https://github.com/user-attachments/assets/c4a76654-b4bc-4f3d-a8ad-a00073ec5674"
/>

### After
<img width="408" height="126" alt="Screenshot 2026-07-30 at 8 26 15 AM"
src="https://github.com/user-attachments/assets/ba2ec249-0520-42ca-87d1-c272515bddef"
/>


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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23539?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-08-05 08:45:46 +00:00
nitin 6e1c710a7d Fan out Fireflies backfill into batched jobs (#23622)
Backfills Fireflies calls missed by webhooks as a fan-out of enqueued
batch jobs.

- `POST /fireflies/backfill { days }` (capped at 3650) validates the
request, enqueues a background discovery worker, and returns immediately
with `{ outcome: 'started' }`.
- The discovery worker and the daily 03:00 healer (fixed seven-day
window) list Fireflies transcript ids for the window, split them into
batches of 20, and enqueue one import job per batch. Discovery reports
transcript, batch, and successfully enqueued batch counts, including
partial enqueue failures.
- Listing is bounded to 2,000 pages. Batch delays are staggered by 60
seconds and capped at the queue's seven-day scheduling horizon.
- Each batch job syncs only its explicit id list, importing only missing
transcript/summary fields; already-synced calls short-circuit.
- Batch jobs use `retryLimit: 2`; a Fireflies 429/5xx fails the job so
the queue retries it. Calls within a batch stay paced one second apart.

Supersedes #23623.

---------

Co-authored-by: martmull <martmull@hotmail.fr>
2026-08-04 12:18:49 +00:00
Raphaël Bosi 167d684a29 Run the people-data-labs spec suite in CI (#23743)
The PDL app's unit vitest config only matched `src/**/*.test.ts`, but
the app's test files were named `*.spec.ts`, so CI ran a single file and
368 tests never executed.

Renamed the 88 spec files to `*.test.ts`, the convention every other
public app and the `create-twenty-app` scaffold already use, which
leaves `vitest.unit.config.ts` byte-identical to the other apps.
`capitalize-name` had both a spec and a test file covering the same
function, so the more thorough one was kept.

Turning the suite on surfaced one real failure: `collectUuids` in the
select-option test scooped up the `path` strings added to
`PDL_LOGIC_FUNCTION_CONSTANTS` and asserted they were v4 UUIDs. It now
stops at any object with a `universalIdentifier` and collects only that
value.

`yarn test:unit` is green at 88 files / 368 tests, with `yarn typecheck`
and `yarn lint` clean.
2026-08-04 12:01:40 +00:00
Abdul Rahman 5ffa121e59 feat(slack): implement channel welcome message functionality (#23699)
https://github.com/user-attachments/assets/a77aa941-da48-4c30-8e14-587516c19ac4


Added a new feature that allows the bot to introduce itself when added
to a Slack channel. This includes a welcome message and a detailed
thread reply outlining its capabilities. The implementation includes new
utility functions for handling the welcome event, managing welcome
state, and posting messages. Updated relevant logic functions to support
this feature, ensuring the bot can provide a seamless introduction to
users in new channels.

- Introduced `slack-channel-welcome` logic function.
- Added constants for welcome message text.
- Implemented event parsing and handling for `member_joined_channel`.
- Updated `slack-events-resolver` to route welcome events appropriately.
2026-08-03 19:08:40 +00:00
nitin b28fc54b44 Classify Recall no-capture sub codes as NOT_RECORDED in call-recorder app (#23693)
Second part of twentyhq/core-team-issues#2706, following #23478 which
shipped the NOT_RECORDED status and workspace upgrade: the call-recorder
app now classifies benign no-capture outcomes (bot never admitted,
meeting not started, nobody joined) as NOT_RECORDED instead of FAILED.

- Parse status sub codes from Recall webhooks and bot snapshots, and map
no-capture sub codes to NOT_RECORDED with the sub code stored as the
failure reason
- Derive NOT_RECORDED from bot snapshots during sync when the bot
finished without a recording and a no-capture leave is in its history
- Treat NOT_RECORDED as terminal alongside FAILED: no artifact-import
completion, no late-event flips between the two; calendar reconciliation
may reset it to SCHEDULED for upcoming meetings
- Prefer the sub code over the status code in FAILED reasons
- Bump the app to 1.6.0 and require twenty >=2.26.0, where the status
exists

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23693?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-08-03 18:19:34 +00:00
Paul Rastoin ebe816abb5 Remove outdated index view pitfall from app agent docs (#23667)
## Context

Index views are now auto-generated by a metadata side effect when an
object is created, so the "Common Pitfalls" entry warning against
creating an object without an associated index view is obsolete.

## What changed

Removed the line "Creating an object without an index view associated.
Unless this is a technical object, user will need to visualize it."
from:

- the `create-twenty-app` template
(`packages/create-twenty-app/src/constants/template/AGENTS.md`), used
for newly scaffolded apps
- the 14 existing copies across `packages/twenty-apps` (`AGENT.md` /
`AGENTS.md` / `CLAUDE.md` / `LLMS.md` in `examples`, `public`, and
`internal` apps)

The other pitfalls (navigationMenuItem, front-component scroll) are
unchanged since they still apply.

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23667?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-08-01 15:04:30 +00:00
martmull 6f361a9bf2 refactor(last-contact): drive backfill with enqueued jobs (#23646)
## What

Reworks the last-contact backfill, which previously ran an orchestrator
that called its own HTTP route in a recursive loop with blocking
`sleep`s, into a single upfront fan-out of enqueued jobs.

On install, `backfill-last-contact` counts people, opportunities and
companies, then enqueues one job per record batch (`ceil(count /
batchSize)`) for each. Each batch job receives its `batchId` in its
payload and processes the matching record window via offset pagination
(`first`/`offset` with a stable `createdAt, id` ordering). No
self-calling loop, no blocking sleeps, no per-batch chaining, no kv
progress state.

Phases don't need to run in sequence: each phase recomputes last-contact
from source interactions (message and calendar participants), not from
the person's stored field, so the jobs are independent and safe to run
concurrently.

## Changes

- `backfill-last-contact` (post-install): counts each phase and fans out
all batch jobs via `enqueueJob`; timeout raised to 300s. Enqueues are
concurrency-limited and staggered with `delayMs` (from
`LAST_CONTACT_BACKFILL_SLEEP_MS`) so thousands of jobs don't all become
eligible at once.
- `src/utils/enqueue-backfill-jobs.ts` (new): counts a phase via the
connection `totalCount`, builds the batch plan, and enqueues the jobs.
- `src/utils/backfill-batch-args.ts` (new): `first`/`offset`/`orderBy`
window for a given `batchId`, ordered by `createdAt, id` so offsets stay
stable while the backfill runs.
- `backfill-{people,opportunities,companies}-last-contact`: handlers now
take `{ batchId }`, query their offset window, process it, and return `{
batchId, count }`. No HTTP route triggers, no cursor/kv state.
- Removed the chaining util (`advance-backfill.ts`) and the
now-purposeless kv-state debug helper (`backfill-get-state.ts`).
- Bumped `twenty-sdk`/`twenty-client-sdk` to `^2.26.0` (first published
version exporting `enqueueJob`), app version to `1.2.3`, plus changelog.
Updated the server-variable copy.

## Testing

- `yarn typecheck` clean
- `yarn lint` clean (0 warnings, 0 errors)
- `yarn test:unit` green (38 passed)
2026-07-31 15:53:14 +00:00
Rashad Karanouh 0f8c227105 v1.6.1 — fix(partners): scope Application RLS to the partner's own partnerUser (#23597)
**Version:** twenty-partners `1.6.1` (`on-application-created` gains
behaviour; no schema change).

## The bug

On https://partners.twenty.com every partner could list **all**
applications, not just their own.

The Partner role's row-level predicate on `Application` was:

```
(partnerUser IS the current member)  OR  (lastActivityAt IS EMPTY)
```

The `IS EMPTY` branch was an insert escape hatch: the Apply workflow
creates the row before `on-application-created` can stamp it, and RLS
validates an insert against the row as submitted.

Only that self-apply path ever writes `lastActivityAt`
(`resolve-candidacy.service.ts`). Every other creation — admin invite,
TFT import, seed — returned early, so `lastActivityAt` stayed `null`
forever and the hatch never closed. All 7 applications in production had
`lastActivityAt = null`, which made the whole table readable by every
partner.

## The fix

1. **Drop the OR group.** `application` becomes a plain `partnerUser IS
the current member` predicate, like the other partner-scoped objects.
The upsert reconciles per (role, object), so the stale group and
predicate are soft-deleted on re-run — a leaking workspace self-heals.
2. **Keep admin invites visible.** The OR branch was also the only
reason an admin-created invite reached its recipient.
`resolve-candidacy` now stamps `partnerUser` from the partner on the
admin path, instead of returning early.
3. **Backfill the rows created before the narrowing.** Neither writer
covers an application an admin created for an already-linked partner;
those existed only behind the leak. `stampPartnerUserFromPartner` now
covers `application` (its three copy-pasted branches collapsed into an
accessor map routed through `shared/graphql/`), and the walk runs from
the app's post-install logic function, gated on `previousVersion <
1.6.1`. No manual step.
4. **Preserve the insert path.** The Apply workflow must map exactly
Opportunity + Partner User. `partnerUser` is writable at insert only
because the server exempts RLS predicate fields there
(`permissions.utils.ts`, insert case only); every other Application
field is locked, so mapping `State` fails the insert — and `state`
already defaults to `APPLIED`.

## Order of operations, per workspace

1. Publish the Apply workflow with the Partner User mapping (edit it if
it already exists).
2. `yarn rls:configure` (`:prod`).

`app:install` stamps the pre-existing rows before step 2 runs, so no
window exists where a partner reads nothing. The script prints these
steps before and after its writes, because the deploy path never opens
the runbook.

## Verification (local bundle, real Partner-role account)

| Case | Result |
|---|---|
| Another partner's application | not visible |
| Own application (`lastActivityAt` null) | visible |
| Admin invite created with `partnerUser` null | stamped from the
partner within seconds |
| All applications stripped of `partnerUser`, then upgraded from 1.6.0 |
post-install returns `{ stamped: 3 }`; the 4th belongs to a partner with
no member |
| Upgrade from 1.6.1 | post-install returns `{ skipped: true }` |
| Insert without `partnerUser` | rejected — *Record does not satisfy
row-level security constraints of your current role* |
| Insert with `partnerUser` = self | accepted |
| Insert with `state` mapped | rejected — *no permission to write field
"state"* |

Lint 0, typecheck 0, 221 unit tests.

## Production notes

- The fix lands on prod by running `yarn rls:configure:prod`. Installing
this version alone does not narrow the predicate.
- The 7 production applications were already backfilled by hand; the
post-install hook makes that reproducible for any other workspace.

## Out of scope

- Creating an OR predicate group fails on server 2.23.2 in a fresh
workspace (`Migration action 'create' for
'rowLevelPermissionPredicateGroup' failed`). Pre-existing and unrelated;
production is unaffected because its `opportunity` group already exists.
It does block `rls:configure` on newly provisioned local bundles.
- Partners still see the `Matching Admin Workspace` navigation folder.
Navigation menu items cannot be scoped by role in the SDK; the views are
row-filtered.
- `configure-partner-rls.ts` should not exist. #21919 made
`rowLevelPermissionPredicates` declarable on the role manifest, and the
SDK we depend on already ships it, so the predicates belong in
`partner.role.ts`. The predicates on the workspace today were written
through the metadata API and are not app-owned, so adopting them needs
its own migration and test pass. Follow-up.
- Deferred cleanups are listed in the thermo review comments below.
2026-07-31 15:35:21 +00:00
martmull 2068bb65b4 Update slack app naming (#23650)
as title

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23650?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 14:38:33 +00:00
Rashad Karanouh 4f429a3976 v1.6.0 — Partners: let partners set the regions they serve on My Profile (#23605)
**Partners app version: `1.6.0`** (minor — new backwards-compatible
field, no schema change)

## Problem

`Partner.region` is a `MULTI_SELECT` (`EUROPE`, `US`, `LATAM`, `MENA`,
`APAC`, `AFRICA`) that was **readable everywhere but writable nowhere**:

- the public partner marketplace **filters** on it
(`filter-partners.ts`) and **displays** it (`PartnerProfile.tsx`)
- the self-service loader already selected and mapped it
(`find-my-partner-profile.ts`, `get-my-partner-profile.mapper.ts`)
- but the **My Profile form never rendered it**, and
`save-my-partner-profile.mapper.ts` uses a `.strict()` zod schema that
rejected the key outright

The only way a partner got a region was `deriveRegion(country)` at
application intake — a single region, derived from their home country,
set once. So a partner serving several regions could not say so, and
anyone who applied without a mapped country had a null region and was
invisible to every region filter on the marketplace.

## Change

"Regions served" becomes partner-editable, in the Location section of My
Profile under Country/City.

| File | |
|---|---|
| `self-service/constants/my-profile.constants.ts` | six region options,
mirroring `partner.object.ts` |
| `self-service/mappers/save-my-partner-profile.mapper.ts` | schema key,
value validation, mapping onto the update payload |
| `front-components/my-profile/profile-form.ts` | **new** — the form's
pure helpers, extracted so they can be unit-tested |
| `front-components/my-profile/profile-form.test.ts` | **new** |
| `front-components/my-profile.front-component.tsx` | the
`ChipMultiSelect` field; ~110 lines lighter after the extraction |

The extraction follows the pattern already used by
`my-case-studies/case-study-rows.ts` and its co-located test. It is a
separate, behaviour-free commit (`321f2c42`) to keep it reviewable apart
from the feature.

## Deliberately out of scope

- **No country → region coupling.** Country is where you are; region is
where you sell. Changing Country does not re-derive, seed, or clear
Region. `deriveRegion` stays intake-only.
- **No backfill.** Existing partners with a null region keep it until
they edit their profile.
- **No marketplace or `completenessScore` change** — both already handle
`region` correctly.
- **No object/schema change.** `yarn twenty plan` reports `0 to add, 3
to change, 0 to destroy` (two logic-function checksums plus the
front-component checksum).

## Permissions

This does not widen partner privileges. `region`'s field UUID is absent
from the locked-field list in `src/roles/partner.role.ts`, so the
partner role already permitted region writes on the partner's own record
via the CRM page — this only surfaces it in the self-service form. The
save path resolves `partnerId` from the request JWT
(`resolve-partner-from-request.service.ts`); the body never supplies a
record id, and `.strict()` rejects one if sent.

## Verification

- `yarn test:unit` — 217/217
- `yarn lint` (oxlint) — 0 warnings, 0 errors
- SDK build typecheck — passes
- Verified end to end against a local workspace: the field renders with
six chips, pre-selects from the stored value, saving two regions
persists `["EUROPE","MENA"]`, and deselecting all persists `[]` while
leaving name/city/country untouched.

## Known, pre-existing, not addressed here

`toMoneyField` round-trips the stored `currencyCode`, but
`saveProfileSchema` pins `currencyCode: z.literal('USD')`. A partner
whose `hourlyRate` was set to a non-USD currency in the CRM therefore
has **every** profile save rejected, with no currency picker in the UI
to correct it. Surfaced while reviewing this branch; it predates it and
is left for a separate fix.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23605?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 13:42:32 +00:00
martmull 50adf4fa8c Remove shouldRunOnVersionUpgrade (#23641)
remove shouldRunOnVersionUpgrade 

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23641?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 13:09:28 +00:00
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
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 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
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
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
nitin c862a2a43d Switch call recorder post-call transcription to Gladia with code switching (#23532)
## What

Switches the Call Recorder app's post-meeting transcription provider
from Recall.ai's built-in transcription (`recallai_async`) to Gladia
(`gladia_v2_async`), with code switching enabled so mixed-language calls
transcribe correctly.

## Changes

- `create_transcript` requests now send `provider: { gladia_v2_async: {
language_config: { code_switching: true } } }` instead of
`recallai_async` with `language_code: 'auto'`. Gladia auto-detects the
spoken language by default, and code switching re-detects it per
utterance for calls that mix languages.
- The provider payload is extracted into a
`RECALL_ASYNC_TRANSCRIPT_PROVIDER` constant so a future
provider-selection variable can slot in without touching the request
code.
- SETUP.md documents the new operational requirement: a Gladia API key
must be added in the Recall.ai dashboard (Transcription > Gladia) for
each region in use, otherwise transcripts fail.

<img width="2810" height="1656" alt="CleanShot 2026-07-30 at 15 00
27@2x"
src="https://github.com/user-attachments/assets/c702ab09-eea8-4c54-8e5a-4941951391c9"
/>

tested on twenty dev recall workspace 

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23532?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 10:42:14 +00:00
Abdul Rahman 72322a4d72 feat: Slack conversational assistant (#22984)
## Summary

Lets workspace members talk to the Twenty CRM agent from Slack —
`@mention` the bot in a channel or DM it, and it answers in-thread using
the `slack-assistant` agent and its assigned role.

## How it works

Slack Events webhook → app route verifies signature → **ack in <3s** and
enqueue a `slackAssistantRequest` → worker posts a placeholder
immediately, then fetches recent thread/DM history (excluding the
current message and placeholder), runs `runAgent`, and updates the
placeholder with the answer. After a successful reply, the thread stays
subscribed (24h TTL, renewed on each reply) so follow-ups work without
re-mentioning.

## App-owned orchestration

Protocol + orchestration live in `twenty-apps/public/twenty-slack`
(events resolver, enqueue, worker, team claim KV, thread subscription).
The server provides shared primitives (app routes, `runAgent`, app KV,
connection OAuth).

## Notes

- Agent role is bound via `roleUniversalIdentifier` on install. Default
**Slack Assistant** role: read/create/update/soft-delete on people,
companies, opportunities, notes, and tasks; **workspace members stay
read-only**; hard destroy stays off. Admins can tighten the role in
Settings.
- Setup (signing secret, event subscriptions, scopes) is in the app
README.
- Long-lived Slack bot tokens (no refresh token) are treated as
non-expiring.
- Multi-turn: recent Slack thread/DM messages are prepended into the
agent prompt.
- Replies are non-streaming for now (placeholder + final `chat.update`);
progressive streaming is a follow-up.

## Follow-ups

- **Streaming replies** — progressive edits while the agent runs.
- **Per-user / per-channel permissions** — Slack→Twenty user mapping and
optional channel rules (open by default; admins can narrow).
- **Other platforms** — Discord/Teams can reuse the same patterns; only
Slack protocol is in this PR.

## Screenshots


https://github.com/user-attachments/assets/3a72770a-93fa-411d-b4aa-2f741afbcee1


<img width="426" height="686" alt="Screenshot 2026-07-27 at 3 58 38 PM"
src="https://github.com/user-attachments/assets/b0a62e7c-c5e4-4c96-9389-5e47d7ef8c77"
/>
<img width="1053" height="726" alt="Screenshot 2026-07-29 at 12 54
45 AM"
src="https://github.com/user-attachments/assets/4e14b3fb-fbe5-4f4d-a380-cc45cc60a01a"
/>
2026-07-30 09:36:38 +00:00
Paul Rastoin 0c545bcdeb [BREAKING-CHANGE] Centralize system View viewField side effect (#23081)
# Introduction

Closes https://github.com/twentyhq/core-team-issues/issues/2669

Part of the `isSystemSideEffect` engine-ownership effort. Until now, a
custom object's default **INDEX** table view (`All {objectLabelPlural}`)
and its view fields were built imperatively in `ObjectMetadataService`
with random `v4()` identifiers, while `twenty-standard` authored its own
copies with hardcoded literals. The two never converged, an object
rename could drift the view, and nothing marked these rows as
engine-owned.

This PR makes the metadata side-effect engine the **single owner** of
the INDEX view and its view fields, on name-free deterministic
identifiers, for custom and standard objects alike.

## Core design

- **Name-free deterministic identity.** The INDEX view identifier
derives from `object identifier + ViewKey.INDEX`
(`getSystemViewUniversalIdentifier`); each view-field identifier derives
from `view identifier + field identifier`
(`getViewFieldUniversalIdentifier`). An object rename (with a pinned
object identifier) keeps the same view, losslessly.
- **`isSystemSideEffect: true` is provenance.** Every INDEX view / view
field the engine emits is flagged system-owned, so manifest deletion
inference never drops it. The flag follows the view: a view field
inherits its parent view's flag.
- **The engine is the sole owner of the INDEX view.** It always emits
it; a caller providing one with the same derived identifier is a genuine
conflict surfaced by the engine's reserved-identifier collision, not
silently deferred.

## Changes

### Shared (`twenty-shared`)

- `getIndexViewUniversalIdentifier` →
`getSystemViewUniversalIdentifier`, now taking a `viewKey` (generalizes
to any singleton engine-owned view).
- Standard field identifiers extracted into a new
`STANDARD_OBJECT_FIELDS` constant, so both an object's `fields` and its
INDEX view read the same field identifiers.
- `buildStandardObjectIndexView` derives the standard INDEX view +
view-field identifiers from `STANDARD_OBJECT_FIELDS`, replacing the
hardcoded literals in `standard-object.constant.ts`.

### Metadata side-effect engine (custom objects)

- **`objectSystemFieldsAndIndexViewOnCreate`** (replaces
`objectSystemFieldsOnCreate`): on object creation, provisions the 7
reserved system fields **and** the INDEX view with one view field per
displayable system field, all `isSystemSideEffect: true`.
- **`fieldIndexViewFieldOnCreate`** (new): on field creation, provisions
the field's INDEX view field. Object created in the same batch →
visible, positioned before the system view fields; pre-existing object →
hidden, appended (preserving the historical `createOneField` behavior).
Both branches resolve the INDEX view by its derived identifier (single
map access, never a scan).
- **`fieldSystemViewFieldsOnDelete`** (new): on field deletion,
cascade-deletes every engine-owned view field displaying it.
- **`objectSystemSideEffectsOnDelete`** (extended): now also
cascade-deletes the object's engine-owned views and their view fields
(in addition to system fields, indexes, searchFieldMetadata). Every
lookup walks a foreign-key aggregator down from the deleted object, so
the work is proportional to what the object owns, never to workspace
size.
- Object-create and field-create positions are derived from the same
caller-input field list, so the INDEX view layout is contiguous with no
handler-ordering dependency.
- `view` / `viewField` added to the side-effect companion metadata names
for `fieldMetadata` and `objectMetadata`.

### Reserved-identifier invariant

A caller can never define an entity whose identifier collides with one a
system side effect produces: caller inputs are forced
`isSystemSideEffect: false` at every entry point (API and app-manifest
transpilers), and the engine raises
`RESERVED_SYSTEM_UNIVERSAL_IDENTIFIER`, aborting the operation, when a
system emission lands on a caller-claimed identifier. Covered by a new
engine-level test.

### Caller-side provisioning removed

The imperative INDEX view + view-field provisioning is removed from
`ObjectMetadataService.createOneObject`. The record-page `FIELDS_WIDGET`
view is intentionally left caller-side and deferred to the follow-up
(see below).

### `twenty-standard` convergence

Standard INDEX views and their view fields converge on the same
derived-identifier + `isSystemSideEffect: true` scheme as the engine.
`twenty-standard` syncs through the from/to migration path (which never
runs the side-effect engine), so it authors this INDEX surface itself,
matching what the engine produces for custom objects.

## Rollout

Two `2.26.0` workspace commands, running after the `2.25`
messageCampaign commands:

- `upgrade:2-26:reconcile-index-view-universal-identifier` re-owns the
INDEX views of the **twenty-standard and workspace-custom applications**
and all their view fields to the derived identifiers with
`isSystemSideEffect: true`, in a single per-workspace transaction. Each
view field identifier is keyed on the application of the **displayed
field** (an app or user column on a standard INDEX view converges too).
Soft-deleted views and view fields are skipped: one can coexist with an
active successor on the same derivation inputs and both would derive the
same identifier. Children reference the view by primary key, so the
re-own is lossless.
- `upgrade:2-26:demote-and-backfill-application-index-view` handles
**manifest-installed applications**, which never had their INDEX view
auto-provisioned: every caller-authored INDEX view of another
application is demoted to `key: null` (a plain additional view under its
manifest identifier), then every application object gets the
engine-owned INDEX view and its full view-field layout backfilled
through the migration pipeline's legacy path (no side-effect expansion),
views committed before view fields across applications since a view
field belongs to the application owning its field. Idempotent and
retry-safe: engine-owned INDEX views are neither demoted nor
re-backfilled, and view creation and view-field creation are gated
independently, so a retry after a partial failure still backfills the
missing view fields of an already-committed view.

Both support `--dry-run` and invalidate the full flat-maps closure
(parents aggregate the re-owned identifiers, children resolve them as
universal foreign keys, and page-layout widget universal configurations
resolve view PKs at cache-build time).

The `2.25` `upgrade:2-25:add-message-campaign-name-field` command is
adapted to resolve the campaign INDEX view by its INDEX key on the
object instead of by universal identifier: it now runs before the
reconcile, on workspaces still holding legacy identifiers.

## ⚠️ Breaking change

This PR **mutates 187 previously hardcoded universal identifiers** — the
standard objects' INDEX views and their view fields (the literals
removed from `standard-object.constant.ts`), now derived.

- **Handled by the `2.26` commands above** for all existing workspaces.
- **The INDEX key is now engine-reserved.** The flat view validator
rejects caller-created INDEX views (API and manifest inputs are forced
`isSystemSideEffect: false`) and enforces a single non-deleted INDEX
view per object; `view.key` is no longer a comparable/updatable
property, so no writer can promote or demote a view after creation.
`ViewManifest.key` is deprecated and ignored (manifest views are always
additional views, so old apps keep syncing and demoted views are not
promoted back); the REST/GraphQL create path now rejects `key: INDEX`.
In-repo example apps (`hello-world`, `document-generator`) no longer
declare it.
- **12 declared-but-never-seeded standard INDEX view field identifiers
deleted** (the former `preservedViewFields` on `timelineActivity`,
`workflowRun` and `workspaceMember`): after the reconcile, no workspace
row references them.
- **`computeFlatViewFieldsToCreate` now derives view field identifiers**
instead of drawing `v4()` ones, which also changes what the committed
`1-23` record-page backfill produces going forward (deliberate,
documented in-code).
- **Record-page views and view fields are not affected** (identifiers
unchanged).
- **In-repo apps: `twenty-last-contact` updated.** It was the only app
declaring explicit INDEX view fields (10 columns across `allPeople` /
`allCompanies` / `allOpportunities`) through manifest `viewFields`.
Those target identifiers are now engine-owned and derived, so the
manifest inputs no longer resolve and install failed with `View not
found`. The app now declares only its fields; the engine's
`fieldIndexViewFieldOnCreate` provisions the matching INDEX view field
automatically. No other app under `packages/twenty-apps` references any
of the 187 mutated identifiers, and apps that target standard views
point at record-page views (e.g. `real-estate` →
`opportunityRecordPageFields`) or their own objects (`twenty-partners`),
all unchanged.

### Loss of granularity for app maintainers

The engine now owns the INDEX view field of every field a caller adds to
an object, so app maintainers lose direct control over those columns.
Previously an app could target the engine-owned INDEX view with an
explicit manifest `viewField` and set its `position` and `isVisible`.
Now `fieldIndexViewFieldOnCreate` appends a **hidden** view field in
caller-input order on field creation, so:

- Columns an app previously showed at a **dedicated position** and
**visible** (e.g. `twenty-last-contact`'s last-contact columns) become
**hidden** and **appended in input order** after install.
- There is currently **no manifest way to override** the
engine-provisioned INDEX view field's position, visibility, or size.

This is a deliberate regression accepted for the sake of
single-ownership, and app maintainers should expect their INDEX columns
to move/hide after upgrading. A follow-up override API will let
maintainers reclaim per-field control over the engine-provisioned INDEX
view field.

## Testing

- Unit specs for each handler: object create (system fields + INDEX
view/view fields, override, position offset), field create (same-batch
vs existing-object, non-displayable noop, no-INDEX-view noop), field
delete, object delete (fields/indexes/searchFieldMetadata/views/view
fields cascade, reverse-relation view field on another object).
- Engine-level test for the reserved-identifier collision.
- `twenty-standard` guard test that its INDEX views/view fields stay on
the derived scheme and stay system-owned.
- Integration test: full engine provisioning of the INDEX view/view
fields on object creation, same view id preserved across an object
rename, and cascade delete on object deletion.

## Follow-up

The full record-page stack (record-page view, its view fields, view
field groups, page layout / tab / widget) is still built imperatively
and moves into the engine in
https://github.com/twentyhq/core-team-issues/issues/2721.
2026-07-29 13:32:21 +00:00
nitin 7b81d9ab83 Fix call-recorder REC badge rendering as empty boxes (#23415)
## What was wrong

The bot camera image draws a "REC" pill on top of the workspace logo.
The label used an SVG `<text>` element, and sharp resolves SVG text
through the host's fonts. The runtimes that execute app logic functions
ship no fonts, so every character fell back to an empty box: the badge
showed "▯▯▯" instead of "REC" in real meetings. It looked fine locally
because dev machines have fonts.

## The fix

Draw the label as vector outlines instead of text. "REC" is outlined
once from Inter SemiBold and stored as an SVG path constant, so the
badge renders the same on any host with no font lookup. The pill width
is derived from its contents instead of hardcoded, and tests fail if
`<text>` or `font-family` ever comes back.

<img width="2120" height="1191" alt="CleanShot 2026-07-28 at 19 16 46"
src="https://github.com/user-attachments/assets/c5fa0958-35ba-48da-be9c-a6af81ec2fa0"
/>


1 -- the bug on prod
2 -- how it looks when its not bugged on prod
3 -- this branches changes

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23415?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-28 14:08:39 +00:00
Rashad Karanouh fea06bdd4b v1.5.1 — partners: Discord notification for client briefs + referring-partner attribution (#23344)
**Merge after #23295.** Targets `main`, but must land second: #23295
bumps `1.3.2 → 1.4.0`, and this bumps `1.4.0 → 1.5.1`. Merging this
first would leave `main` at 1.5.1 and make #23295's bump conflict and
regress the version. `package.json` is the only file the two branches
share.

App version: **v1.5.1**.

## What this does

Posts a Discord notification when a client brief is submitted through
the public marketplace form, and records which partner's profile page
the brief came from.

A visitor can reach the brief form from the marketplace listing page or
from a specific partner's profile. Until now that context was lost. This
adds a `referredByPartner` relation on Opportunity so the attribution is
a queryable CRM fact rather than a line in a chat message.

## How it works

`submitClientBrief` resolves the incoming `partnerSlug` to a Partner,
sets the relation on create, then posts the embed inline.

Inline rather than an `opportunity.created` database trigger, because
that event cannot distinguish a brief from a TFT import — both are
created by logic functions and both carry `createdBy.source ===
'APPLICATION'`. A trigger would need a discriminator like "source is
APPLICATION and `tftOpportunityId` is empty", which silently breaks the
day a third logic function creates an Opportunity.

The cost of going inline is that the Discord call sits in the visitor's
request, so it uses a 3s timeout rather than the trigger path's 8s, and
every failure is swallowed — a dead webhook can never turn a submitted
brief into a failed one.

## Notable decisions

- **Slug resolution ignores `validationStage` and `availability`**,
unlike the marketplace profile query. If someone submitted a brief from
a partner's page, that partner referred it, even if they go unavailable
a minute later. Filtering would silently drop real attribution.
- **An unresolved slug never fails the brief.** It logs a warning,
leaves the relation unset, and still notifies. A brief is a sales lead;
losing one over an attribution field the visitor never saw would be a
bad trade.
- **`referredByPartner` is separate from the existing `partner` field.**
One is who sent the lead, the other is who works it.
- **The Discord connector moved to `modules/shared/connector/`.** Two
domains now need it, and `AGENTS.md` forbids importing logic sideways
between domains. `postWebhook` gained `label` and `timeoutMs`
parameters; the transport is otherwise unchanged.
- Reuses the existing `DISCORD_WEBHOOK_URL` and
`PARTNER_APP_FRONTEND_URL` variables — no new configuration to set on
prod.

## Permissions

`partner.role.ts` locks the new Opportunity field.
`configure-partner-rls.ts` treats its skip-list as a closed allowlist of
system columns, so an unlocked new field is reported as a discrepancy.

Note that Opportunity RLS for partners is `(partnerUser IS me) OR
(isListed = true)`, so on a **listed** brief any partner can read
`referredByPartner` — i.e. see that a competitor referred it. Called out
deliberately; happy to restrict it if that's not wanted.

## Testing

8 unit tests for the embed mapper (partner present/absent, truncation,
absent optionals, no email in the payload, inline-row padding) and 4 for
the schema. Full suite: 188 passing, lint clean.

Verified end to end against a local workspace with a real Discord
webhook. All three paths return `ok: true`; the persisted relation was
confirmed via GraphQL rather than inferred from the status code:

| Submission | `referredByPartner` |
|---|---|
| valid slug | linked to the partner |
| no slug | `null`, embed reads "Marketplace listing" |
| unknown slug | `null`, brief still succeeds |

## Follow-up, not in this PR

`yarn rls:configure` fails before reaching its field-lock check — its
retry path strips `predicateGroups` but the predicates still carry
`rowLevelPermissionPredicateGroupId`, so the retry fails identically.
Pre-existing and unrelated to this change (`configure-partner-rls.ts` is
untouched here), but it means the script cannot currently verify the
lock on a fresh workspace.

The website side that sends `partnerSlug` is #23351. Until it ships,
this is inert: no caller sends the field, and briefs behave exactly as
before. Merge this one first — #23351 is the sender, this is the
receiver.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23344?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-28 09:42:23 +00:00
martmull 1f55234d0b fix(call-recorder): leave call when only recording bots remain (#23053)
## Problem

Fixes
[core-team-issues#2689](https://github.com/twentyhq/core-team-issues/issues/2689).

`everyone_left_timeout` only fires when the bot is the sole remaining
participant, and Recall counts other recording bots as participants. So
when several bots share a meeting, none of them sees itself as alone.

Recall does enable `bot_detection` by default, but it ships an empty
`matches` list, so the name-based check can never classify anyone. The
only detector that actually runs is the behavioural one, at its default
20 minute grace plus 10 minute timeout. A meeting left with only bots
therefore stays open for around 30 minutes, and two Twenty bots in the
same call never recognise each other at all.

This happens when several workspace members are invited to the same
meeting and each has the recorder preference on, or when third-party
notetakers stay behind after the humans leave.

## What this does

Sends a full `automatic_leave.bot_detection` block plus
`silence_detection`:

- **`using_participant_names`** — the configured recorder name, so
co-scheduled Twenty bots recognise each other, plus a list of common
notetakers. `timeout: 10`, which is Recall's enforced minimum; their
example config shows `5` and the API rejects it.
- **`using_participant_events`** — a participant that never speaks nor
shares screen is treated as a bot.
- **`silence_detection`** — Recall's documented example values
(`activate_after: 1200`, `timeout: 300`). Previously unset, so it fell
back to Recall's 20 + 60 minute default.

Both bot detectors activate 5 minutes after the **meeting start time**,
not 5 minutes after the bot joins. The bot joins early by a configurable
amount, so anchoring to join time spent the grace period before the
meeting existed — at a 10 minute early join, detection would have gone
live 5 minutes before the meeting began.

`everyone_left_timeout` is unchanged and still covers the ordinary case.

Effect:

| | before | after |
|---|---|---|
| Only bots remain | ~30 min | ~5 min after meeting start |
| Someone leaves the call open after talking | ~80 min | ~25 min |

## Deferred

De-duplicating bots per meeting URL, so several `callRecording`s in one
meeting share a single bot instead of each spawning one. `bot_detection`
is still needed for third-party bots, so this ships first.

---------

Co-authored-by: ehconitin <nitinkoche03@gmail.com>
2026-07-28 08:51:40 +00:00
martmull 75e767f08b update exa twenty cli tools (#23379)
as ttitle
2026-07-27 17:17:42 +00:00
martmull b94a889bcb Organize public apps properly (#23376)
remove "twenty-" prefixes from public folders and package names

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23376?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-27 16:20:13 +00:00
Rashad Karanouh e631c986a1 v1.4.0 — partners: auto-link partner user on workspaceMember.created (#23295)
**App version:** `1.4.0` (partners app —
`packages/twenty-apps/internal/twenty-partners`)

## What

Adds partner onboarding auto-linking: when a `workspaceMember` is
created (invite signup), a DB-event-triggered logic function resolves
the partner by the member's email and stamps `partnerUser` across the
partner and its cascade (person, company, links, services, content,
applications).

## Key design decision — data-linking only, no role assignment

The trigger **does not** assign the Partner role. A logic function runs
as an app **agent**, with no user session; `updateWorkspaceMemberRole`
is guarded by `UserAuthGuard` + `AuthWorkspaceMemberId` and is
unreachable from an agent, so the mutation silently no-ops regardless of
permission flags. The dead role code (`ensure-partner-role` service, its
role query/mutation, and the role mocks) is removed so the trigger's
responsibility is unambiguous: resolve partner by email → link
`partnerUser` cascade with retry-on-partial-failure. Role assignment, if
wanted, belongs on the invite path (`sendInvitations` accepts a
`roleId`), not the trigger.

## Changes

- `on-workspace-member-created.logic-function.ts` — DB-event trigger on
`workspaceMember.created`; skips internal (`@twenty.com`) and unmatched
emails
- `resolve-partner-by-email` / `link-partner-user` services + typed
`graphql/` operations for the cascade
- `normalize-invite-email` util
- `partnerUserLinkedAt` field on Partner
- Seed: one contact `Person` (with `partnerId` + email) and one
`Company` per partner so onboarding is testable via a seeded invite
email; drops the `person.city` write removed in SDK 2.25 that broke
`yarn seed`

## Verification

- Unit: **173/173 pass** (27 files) · `tsc --noEmit` clean · `oxlint` 0
warnings/0 errors
- End-to-end: invited + signed in a seeded partner
(`lena@act-education.example`) on the workspace subdomain; the trigger
linked the member to the **Act Education** partner and the self-service
**My Profile** page rendered the linked profile (`POST
/s/my-partner-profile → 200`)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23295?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-27 11:57:23 +00:00
Abdullah. 302f46f0ea fix: bump brace-expansion to 5.0.8 in app lockfiles (Dependabot) (#23346)
## Summary

Bumps **brace-expansion -> 5.0.8** in the three app lockfiles whose copy
sits on the 5.x line, clearing **GHSA-mh99-v99m-4gvg** (high, vulnerable
`<= 5.0.7`) on those manifests:

- `examples/hello-world` (`^5.0.2`)
- `examples/postcard` (`^5.0.5`)
- `internal/self-hosting` (`^5.0.5`)

All three are caret ranges, so a recursive `yarn up -R brace-expansion`
lifts them with **no resolution and no `package.json` change**.

## Why the fixtures are not included

This advisory declares a single vulnerable range, `<= 5.0.7`, which
spans **every** major line - so the `brace-expansion@2.1.2` copies in
`seed-dependencies` and `common-layer-dependencies` are flagged as well.
But **2.1.2 is the last 2.x release** (1.x likewise ends at 1.1.16), and
the only patched version is **5.0.8**. Those consumers declare `^2.0.1`
/ `^2.0.2`, which caps below 3.0.0, so there is no in-range fix:
clearing them would mean forcing a cross-major jump from 2.x to 5.x via
a resolution, which is a behavior risk rather than a mechanical lift.

Same situation for the root alert
([1765](https://github.com/twentyhq/twenty/security/dependabot/1765)),
where `nx` pins `brace-expansion` 5.0.6 exact.

## Verification

- brace-expansion resolves to **5.0.8** in all three lockfiles.
- `yarn install --immutable` passes in each.
- 5.0.8 published 2026-07-23, clears the 3-day npm age gate.
2026-07-27 11:33:38 +00:00
nitin a6b36422f9 Fireflies: upgrade to twenty-sdk 2.23 (#23349)
Fireflies was skipped by both SDK bump sweeps (#23124, #23165) and sat
on `twenty-sdk ^2.18.0` with no `engines.twenty` floor, while the rest
of the published set moved to `2.23.0-alpha.2`.

- `twenty-sdk` / `twenty-client-sdk` `^2.18.0` -> `2.23.0-alpha.2`
- adds `engines.twenty: ">=2.23.0"`

No source changes needed: the 2.19 identifier migration (#22601) only
affected apps referencing a standard object's system-field identifier,
defining a relation into a standard object, or calling the field-UID
derivation helper. Fireflies does none of those.

The `engines.twenty` floor means the app integration job needs a server
image at 2.23+, so it may fail on version mismatch rather than an app
defect, as in #22601.
2026-07-27 10:31:20 +00:00
Abdullah. ed95b8cfde fix: bump postcss to 8.5.22 across app lockfiles (Dependabot) (#23340)
## Summary

Bumps **postcss -> 8.5.22** in the 13 twenty-apps lockfiles that carry
it transitively, clearing **GHSA-r28c-9q8g-f849** (high) on those
manifests: path traversal in previous source map auto-loading
(`sourceMappingURL`) leading to arbitrary `.map` file disclosure,
vulnerable `<= 8.5.17`.

Apps covered: document-generator, hello-world, postcard, self-hosting,
twenty-partners, call-recorder, people-data-labs, twenty-discord,
twenty-exa, twenty-fireflies, twenty-last-contact, twenty-linear,
twenty-slack.

Every app reaches postcss through a caret range (`^8.5.15`), so a
recursive `yarn up -R postcss` lifts it in each project with **no
resolution and no `package.json` change** - the diff is 13 `yarn.lock`
files and nothing else. Yarn resolves to **8.5.22**, the latest in range
(above the 8.5.18 fix floor).

## Not included

The **root lockfile** carries the same advisory but its postcss copies
are held by exact pins - `next` (8.4.31 in every stable release,
including 16.2.11) and `@mintlify/common` (8.5.14, unchanged in its
latest) - so no `yarn up` reaches it. That one needs a scoped resolution
and is handled separately.

## Verification

- postcss resolves to **8.5.22** in all 13 lockfiles; nothing at or
below 8.5.17 remains.
- `yarn install --immutable` passes in each of the 13 projects.
- 8.5.22 published 2026-07-22, clears the 3-day npm age gate.
2026-07-27 08:50:00 +00:00
Abdullah. 155636d7d9 fix: bump tar to 7.5.21 across app lockfiles (Dependabot) (#23332)
## Summary

Bumps **tar -> 7.5.21** in the 13 twenty-apps lockfiles that carry it
transitively, clearing **GHSA-r292-9mhp-454m** (medium) on those
manifests: uncontrolled recursion in `mapHas`/`filesFilter` allows an
uncatchable stack-overflow DoS via a crafted long-path tar with member
selection, vulnerable `<= 7.5.20`.

Apps covered: document-generator, hello-world, postcard, self-hosting,
twenty-partners, call-recorder, people-data-labs, twenty-discord,
twenty-exa, twenty-fireflies, twenty-last-contact, twenty-linear,
twenty-slack.

Every app reaches tar through a caret range (`^7.5.4`), so a recursive
`yarn up -R tar` lifts it in each project with **no resolution and no
`package.json` change** - the diff is 13 `yarn.lock` files and nothing
else.

## Not included

- **Root lockfile**: same advisory, shipped separately in #23330.
- **`application-package/constants/seed-dependencies`**: the 14th
manifest with this advisory. Its `yarn.lock` is checksum-coupled to
`DEFAULT_YARN_LOCK_CHECKSUM`, so it moves in its own PR with the
constant regenerated alongside.

## Verification

- tar resolves to **7.5.21** in all 13 lockfiles; nothing below remains.
- `yarn install --immutable` passes in each of the 13 projects.
- 7.5.21 published 2026-07-21, clears the 3-day npm age gate.
2026-07-27 07:55:04 +00:00
Abdullah. 0d876eb714 fix: lift axios/tar/brace-expansion/body-parser across app lockfiles (Dependabot) (#23267)
## Summary

Sweeps the **twenty-apps lockfiles** for this week's advisory wave:
recursive `yarn up` for **axios, tar, brace-expansion, body-parser** in
each of the 12 apps with open Dependabot alerts (hello-world, postcard,
self-hosting, twenty-partners, call-recorder, people-data-labs,
twenty-discord, twenty-exa, twenty-fireflies, twenty-last-contact,
twenty-linear, twenty-slack).

All moves fit the declared ranges (apps carry these transitively via
`twenty-sdk`, whose `axios ^1.16.0` and deep tar/brace chains are
carets), so the diff is **lockfile-only** across all 12 manifests - no
resolutions, no `package.json` changes. axios -> 1.18.x, tar -> 7.5.20
(critical GHSA-23hp-3jrh-7fpw chain), brace-expansion -> 1.1.16 / 2.1.2
/ 5.0.7, body-parser -> 1.20.6 / 2.3.0.

The second commit narrows scope to apps only: the twenty-server fixture
projects (seed-dependencies, common-layer-dependencies) move to a
dedicated PR because seed-dependencies' yarn.lock is checksum-coupled to
`DEFAULT_YARN_LOCK_CHECKSUM` in
`get-default-application-package-fields.util.ts`; it also drops
accidentally committed `.yarn/install-state.gz` artifacts.

## Deliberately not covered

- **sharp**: every path is minor-locked at `^0.34.5` (including
twenty-sdk latest) - separate PR bumping twenty-sdk's range.
- **react-router / react-router-dom**: no fixed release on the 6.x line
(fix is the v7 major); tracked separately.

## Verification

- Vulnerable-version scan across all 12 lockfiles: no axios <1.18, tar
<7.5.19, brace-expansion below 1.1.16/2.1.2/5.0.7, or body-parser below
1.20.6/2.3.0 remains.
- `yarn install --immutable` passes in each app.
- All fix versions clear the 3-day npm age gate.
2026-07-24 15:21:32 +00:00
martmull 940d150775 chore(self-hosting): upgrade to latest twenty CLI tooling (#23279)
## What

Upgrades the internal `self-hosting` app to the latest Twenty CLI
tooling, bringing it in line with the other actively-maintained apps in
the monorepo.

- `twenty-sdk`: `2.19.0-alpha.1` → `2.23.0-alpha.2` (dependency +
devDependency)
- `twenty-client-sdk`: `2.19.0-alpha.1` → `2.23.0-alpha.2` (dependency +
devDependency)
- `engines.twenty`: `>=2.19.0` → `>=2.23.0`
- Regenerated `yarn.lock` to match.

The `twenty` CLI ships inside `twenty-sdk`, so this pulls the app onto
the same CLI version every other recently-updated app (call-recorder,
people-data-labs, twenty-partners, real-estate, last-contact, postcard)
already uses.

## Verification

- `yarn typecheck` passes
- `yarn lint` passes (0 warnings, 0 errors)
- `yarn test:unit` passes (3/3)

Integration tests (`yarn test`) require a running Twenty server and were
not run in this environment.

## Notes

Scope is limited to the CLI/SDK tooling. Framework deps (React 18) were
left untouched since they are not tied to the CLI version and vary
across apps.

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23279?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-24 14:26:42 +00:00
Raphaël Bosi d1c6b8ee72 Show relation record labels instead of UUIDs in dashboard charts (#23163)
https://github.com/user-attachments/assets/d012a013-2c90-49a1-a27e-b8e4b684a84f



Charts grouped by a relation without a sub-field rendered raw FK UUIDs
on axis ticks, legends and tooltips. The server now batch-resolves the
grouped record ids to their label identifier through a permission-scoped
query and formats every bucket with the record's display name.
Unresolvable records (deleted or not readable) render as Unknown and
their ids are stripped from the response payload. Same-named records get
an ordinal suffix so their buckets don't merge. Covers bar, line and
pie, plain and morph relations.

```mermaid
flowchart TD
    A["Dashboard widget load"] --> B["Chart data service<br/>(bar / line / pie)"]
    B --> C["executeGroupByQuery:<br/>group by relation FK id,<br/>ORDER BY target label identifier,<br/>scoped to source object permissions"]
    C --> D["filterOutEmptyChartBuckets"]
    D --> E{"Bare relation axis?<br/>(no sub-field)"}

    subgraph RL["ChartRelationLabelService.resolveRelationLabels"]
        direction TB
        G1["Collect distinct record ids<br/>per target object"] --> G2["Batch SELECT label identifier columns,<br/>scoped to TARGET object permissions"]
        G2 --> G3["buildRawLabelByRecordId:<br/>display name per record"]
        G3 --> G4["buildUniqueRelationLabels:<br/>suffix duplicates, Unknown for unresolved"]
    end

    E -- No --> H["formatDimensionValue per bucket"]
    E -- Yes --> G1
    G4 --> H
    H --> I["Strip unresolved ids from<br/>formattedToRawLookup"]
    I --> J["Chart DTO to frontend"]
```

The chart settings sub-field dropdown gains a Record option to group by
the related record itself, and now only offers sub-fields the backend
accepts (system fields like a workspace member's updatedBy were
selectable but rejected at query time). Chart-data errors are now logged
server-side.

Also fixes two latent bugs on this path: sorting a bare-relation chart
by field threw `Cannot orderBy unknown field: agentId`, and the pie
chart truncated slices before sorting. The AI dashboard tool guidance
and the seeded dashboards no longer force the sub-field workaround.

The group-by query orders buckets by the related record's label
identifier at the database level (the engine now accepts ordering by a
target field when grouping by its id), so with more than 100 distinct
related records the surviving buckets match the label order.
2026-07-24 14:12:22 +00:00
Rashad Karanouh 18fb0946e6 v1.4.0 — Raise partner bar: Twenty experience fields + triage (#23224)
## Summary
**Version:** `twenty-partners` **v1.4.0** (minor — new Partner fields +
apply contract)

- Add Partner fields `twentyExperience`, `twentyExperienceNotes`,
`twentyExperienceProofLink` and persist them from
`submit-partner-application` (≥200-char narrative at API boundary)
- Surface Twenty experience on applications / validated / per-stage
triage views and the Partner record side panel (drop empty Introduction
from that panel)
- Add pure Tally CSV match/map helpers (ops import script stays outside
the repo) for backfilling existing partners by `partnerId`

**Companion PR (website):** #23223 — Experience step on apply +
thank-you without Cal.

## Test plan
- [ ] `yarn twenty apply -r <remote>` on a workspace — Partner gains the
three experience fields
- [ ] Website apply (with #23223) persists milestones / notes / proof
link on create and email-linked update
- [ ] Applications + Validated views show experience columns; record
side panel lists experience fields
- [ ] `yarn lint` clean; `yarn test:unit` covers schema + map-tally
helpers
- [ ] After Tally campaign: dry-run then apply CSV import via local ops
script under `~/twenty/docs/superpowers-specs/raise-bar-import/`
2026-07-23 21:26:41 +02:00
Rashad Karanouh 9d8ce7c325 v1.3.2 — Modularize partners app into vertical-slice modules/ (#23168)
**Version:** `twenty-partners@1.3.2` (patch — internal refactor, no
visible behavior change)

## What & why

Reorganizes the `twenty-partners` SDK app from a flat, type-first layout
(`src/{objects,fields,views,logic-functions,front-components,…}`) into a
**vertical-slice** layout under `src/modules/<domain>/<feature>/`. Files
that
change together now live together; each SDK entrypoint is a thin
discoverable
shim over a service + graphql-ops + mapper/connector layer.

This is a pure structural refactor — **no object, field, view, enum,
logic
function, trigger, role, or application variable changed.**

## Final layout

```
src/modules/
  shared/        http · services · graphql · utils · front-components · navigation-menu-items (cross-domain nav folders)
  opportunity/   fields·view-fields·views·navigation-menu-items·page-layouts·constants + intake/ + matching/
  partner/       objects·fields·constants·utils + directory/ · self-service/ · marketplace/ · application-intake/ (Discord connector/)
  application/   objects·fields·views·navigation-menu-items·page-layouts + services · graphql
```

Every `defineLogicFunction` entrypoint is now a thin
`*.logic-function.ts`
(all < 40 lines) at its domain/feature root, delegating to a
`*.service.ts`;
graphql operations live in `graphql/{queries,mutations}/`, pure
transforms in
`mappers/`, outbound APIs (the Discord webhook) in `connector/`, pure
helpers
in `utils/`.

## Safety — the load-bearing invariant

The server diffs app primitives by `universalIdentifier`, so a
changed/dropped
UUID would drop-and-recreate the object on prod (data loss). This branch
holds
that line:

- **887 `universalIdentifier`s byte-identical** to the branch base
(every
relocation is a `git mv`; every extracted entrypoint keeps its original
UUID/name/trigger verbatim). Re-verified byte-identical across the
rebase.
- `yarn twenty dev --once` against a live workspace = **"No changes.
Twenty
metadata matches your manifest."**, confirmed idempotent on a second run
—
the whole refactor is a metadata no-op (zero create/delete/identity
change).
- Every extracted graphql op was verified **byte-identical** to its
original
(args, `first:` caps, pagination, selection sets), and the
partner-application
Discord embed's deliberate PII omission (no email / hourly rate) is
preserved.

## Rebased onto latest `main`

This branch is rebased onto `main` (`d20e5378fd`) and now carries main's
`twenty-sdk` / `twenty-client-sdk` **2.23.0-alpha.2** bump.

Note for reviewers: main had independently bumped this package to
`1.3.1`, so
the original `1.3.0 → 1.3.1` commit here was redundant and git dropped
it during
the rebase (`patch contents already upstream`) — with **no textual
conflict**,
since both sides wrote the same version string. The bump is therefore
now
**`1.3.2`**. The rebase touched only `package.json` and `yarn.lock`;
**every line
of refactored source is byte-identical** to the pre-rebase tree.

## Verification

All run on the rebased tree, against SDK `2.23.0-alpha.2` and a live
Twenty
server `v2.23.2`:

| Check | Result |
|---|---|
| `universalIdentifier` set | 887, byte-identical |
| `yarn twenty dev --once` | "No changes" (idempotent on re-run) |
| Typecheck | pass |
| `yarn lint` | 0 warnings, 0 errors (287 files) |
| `yarn test:unit` | 158/158 (23 files) |
| `yarn test:integration` | 45/45 (13 files) |

## Also in this PR

- **Architecture convention doc** — `AGENTS.md` (+ a one-line
`CLAUDE.md` pointer)
at the package root documents the vertical-slice conventions this
refactor
establishes: the layout, the dependency rule (`logic-function → service
→
graphql/connector`), file naming, connector = outbound-only (inbound
webhooks
are logic-functions), and the UUID invariant. It ships here so the doc
and the
  structure it describes land together.
- **`modules/shared/`** dedup: the secret-guarded intake envelope, the
find-or-create-company/person helpers + their graphql ops, `collectAll`
  pagination, `http-url`/`strip-markdown`/`is-non-empty-string` utils.
- **Vitest configs collapsed** into one `vitest.config.ts` with `unit` +
  `integration` projects (`yarn test:unit` / `yarn test:integration`).
- Cross-domain nav folders (`pipeline-folder`,
`partner-workspace-folder`)
  hoisted to `modules/shared/navigation-menu-items/`.

## Deferred (non-blocking, tracked follow-ups)

- Add direct unit tests for the shared `collectAll` / `isNonEmptyString`
utils
  (currently covered indirectly).
- Move `submit-client-brief`'s zod schema out of its mapper file into
its own
  schema file (mirroring the partner side).
- Route `stamp-partner-user-on-child` through the shared self-service
mutation ops.
- `find-partner-by-member.ts` is duplicated identically in the
`application` and
  `self-service` domains; a candidate to hoist into `modules/shared/`.
2026-07-23 13:59:00 +02:00
martmull e0debf87a7 fix(call-recorder): listen proper updated fileds event (#23135)
## Context

Around meeting-end peaks (~6pm), Recall/Svix delivers event bursts for
every recorded call across the 700+ workspaces the app is installed on.
Each delivery was processed synchronously in the API request path, and
internal failures surfaced to Svix as non-2xx, so it redelivered — a
self-feeding storm of 500s and latency that only stopped when the
webhook endpoint was disabled.

## What changed

With #23134, server-route dispatch defaults to **queued** server-side:
the API acks Svix with a 202 right after signature verification,
`process-recall-webhook` runs on the worker queue, and failed runs retry
there (resolver `retryLimit`, default 3). The resolver needs no change
at all — `recall-webhook.ts` is back to main, and no SDK update is
required.

Remaining app changes:
- `schedule-recall-bot-on-call-recording-update` declares
`updatedFields` (the pending-transition fields) on its
`callRecording.updated` trigger, so the server drops the app's own
scheduling-progress and artifact writes **before** spawning a full
execution instead of executing and returning "skipped". The in-handler
check stays as a fallback.
- Version bumped to 1.5.0.
- Code comments introduced by earlier revisions of this PR removed per
review.

## Tests

- New test pins the trigger's `updatedFields` declaration.
- `yarn test:unit` (507 tests), `yarn lint`, `yarn typecheck` all green.

## Notes

- The `call-recorder (dockerhub-latest)` CI leg fails because main
already requires `twenty >= 2.23.0` while the latest published image is
2.22.0 — pre-existing, clears when 2.23.0 images publish.
- The 250s `import-call-recording-artifacts` route still runs in an API
request slot behind the fire-and-forget own-route POST; moving it fully
off the request path is a follow-up.
2026-07-22 14:58:25 +02:00
Paul Rastoin 16e7db8577 PDL react 19 (#23169)
Started to face in version 1.07 on staging 2.23.0
```
Failed to load front component: Cannot read properties of undefined (reading 'ReactCurrentBatchConfig')
```
2026-07-22 14:39:51 +02:00
Paul Rastoin 8d943e1f68 Bump to alpha 2 all published apps (#23165)
Related https://github.com/twentyhq/twenty/pull/23155

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23165?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-22 13:39:06 +02:00
Thomas Trompette 623aac5a56 Show agent names on real estate dashboard charts (#23146)
Follow-up to the real estate demo app: the Agency Overview dashboard's
"Listings by agent" and "Showings by agent" bar charts grouped by the
agent relation, which rendered the agent's UUID on the axis.

Adding `primaryAxisGroupBySubFieldName: 'name.firstName'` groups by the
agent's first name instead, so the charts show agent names (Emma, Lucas,
Chloe, Louis).


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23146?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-22 12:04:23 +02:00
Paul Rastoin c59df4cddd Upgrade remaining official apps to 2.23 alpha (#23124)
The sdk does not provide system field anymore at all ( including
relation ) if you don't upgrade you'll get a deterministic universal
identifier collision from previously provision and now side effect
resulting ones

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23124?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-21 18:59:38 +02:00
Thomas Trompette 9a374c4b6d Add real estate demo app (#23111)
## What

A new app under `packages/twenty-apps/internal/real-estate` that seeds a
real-estate demo workspace: buyers, sellers, agents, property listings,
showings, and an opportunity pipeline, with role-based access.

## Data model

- **Property** (custom object): address, price (currency), status
(coming soon / active / under offer / sold), type, beds/baths/surface,
photos, `listingAgent` and `sellerContact` relations to Person.
- **Showing** (custom object): scheduledAt, status, feedback, interest
rating, and `property` / `buyer` / `agent` / `opportunity` relations.
- **Person** (standard, extended): `personType` (Buyer / Seller /
Agent), budget min/max, pre-approved, desired area.
- **Opportunity** (standard, extended): `buyerStage` pipeline
(completing profile → showing → offer made → closing → won → lost), and
`buyer` / `seller` / `property` / `showings` relations.

## Views

- **Buyer Pipeline** — kanban on Opportunity grouped by buyer stage, one
card per buyer, scoped to real-estate deals.
- **Available (by price)** — properties sorted by price desc, excluding
sold.
- **Agents** / **Buyers** — filtered Person views.
- Record-page layouts for Opportunity and Showing so the relations
render on the detail pages.

## Roles

- **Broker** — full access (default).
- **Agent** — Property / Showing / Person / Note / Task, no Opportunity
access.
- **Seller** — read-only on their listing and its showings.

## Seeding

A synchronous post-install logic function seeds 4 agents, 12 sellers, 12
buyers, 30 properties across 5 cities, 24 showings, and 12 opportunities
(one per buyer), all wired through the relations.

---------

Co-authored-by: prastoin <paul@twenty.com>
2026-07-21 18:53:23 +02:00
Paul Rastoin 2ef7b7824e Upgrade call-recorder, people-data-labs, last-contact and partners apps to twenty-sdk 2.23.0-alpha.1 (#23098)
## What

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

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

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

## How

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

## Breaking change constraints

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

## Validation

- Regenerated the app lockfiles against the published `2.23.0-alpha.1`.
- `people-data-labs` typechecks cleanly against the real `2.23` SDK
types.
- CI: people-data-labs and partners green on `local`, red on
`dockerhub-latest` by design; server/SDK/all other checks green.
- Rebased onto latest `main`.
2026-07-21 15:40:14 +02:00
martmull a0e8d48656 Reduce call-recorder recovery crons to daily to relieve production (#23099)
## Context

Call Recorder is installed on 700+ workspaces and its two recovery crons
run every 15 minutes with the same pattern in every workspace, so all
executions land on the same minute boundaries and impact production. The
`callRecording.updated` event trigger (#23014) now covers the fast path
within seconds; these crons are only backstops for crashed creations and
missed webhooks.

## What changed

Pattern updates only, no logic changes:

- `process-pending-call-recording-requests`: `*/15 * * * *` -> `0 3 * *
*`
- `reconcile-stale-bot-state`: `*/15 * * * *` -> `30 3 * * *`

The daily times are staggered half an hour apart from the existing daily
crons (04:00 upcoming-events sweep, 04:30 orphaned-bots cleanup) so the
four daily jobs never coincide.

## Notes

- Recovery latency for rows missed by the event trigger becomes up to
24h instead of 15min, which is acceptable for backstops (the 7-day
convergence lookback is unaffected).
- Cron patterns live in installed manifests, so existing installations
pick this up on app upgrade only.
- The daily herd across workspaces at 03:00/03:30 remains synchronized
until generic cron spreading lands server-side (#23088 covers only the
`*/5` and `*/15` patterns).

---------

Co-authored-by: martmull <martin@twenty.com>
2026-07-21 12:11:06 +02:00
Paul Rastoin 1be5a0e54a System side effect relations (#22882)
Closes twentyhq/core-team-issues#2667

## What

Default relations to the standard relation objects
(`timelineActivities`, `attachments`, `noteTargets`, `taskTargets`) are
now fully owned by the **metadata side-effect engine**. Neither the API
transpilers nor the SDK manifest builder provision them anymore: any
object creation, rename or deletion — regardless of the caller — goes
through the same engine handlers.

## Why

- Provisioning was duplicated across the API path and the SDK manifest
builder, with diverging behavior.
- Universal identifiers of relation fields were derived from object
**names**, so renaming an object mutated them and forced lossy
delete+create cycles on manifest sync.

## How

### Engine-owned lifecycle (side-effect handlers)

- `objectSystemRelationsOnCreate`: provisions the 8 forward/reverse
relation fields (+ join column indexes) when an object is created.
- `objectSystemRelationsOnUpdate`: renames the reverse morph fields
(`target<ObjectName>`) when their host object is renamed — a lossless
`fieldMetadata.update`.
- `objectSystemSideEffectsOnDelete`: cascades deletion of engine-owned
fields/indexes when the object is deleted.
- The API transpilers and the SDK `buildManifest` no longer inject these
fields; `isSystemSideEffect: true` marks engine-owned entities, guarded
by a granular property allowlist (only `isActive` is user-editable) and
excluded from manifest deletion inference.

### Name-free deterministic universal identifiers

New `getSystemRelationFieldUniversalIdentifier({
applicationUniversalIdentifier, objectUniversalIdentifier,
relationTargetObjectUniversalIdentifier })` in `twenty-shared`, exported
from `twenty-sdk/define`. The identifier is keyed on the two **object**
identifiers instead of field names (direction encoded by argument
order), so object renames never mutate relation field identifiers. It
cannot collide with the name-based `getFieldUniversalIdentifier`
derivation (field names cannot contain `:`).

### twenty-standard re-owned

All 48 forward/reverse system relation field declarations in
`STANDARD_OBJECTS` now pin the derived name-free identifiers (computed
inline via the shared util) and carry `isSystemSideEffect: true`, with
labels/icons declared explicitly (translated via `msg`).
`twenty-standard` is projected as if the engine had generated these
fields itself.

### 2.23 upgrade commands

- `reconcile-system-relation-field-universal-identifier`: structurally
matches existing default relation fields per workspace and backfills the
derived universal identifiers, `isSystemSideEffect` flags, and standard
labels/icons.
- `upgrade-people-data-labs-application`: upgrades installed PDL apps to
`1.0.7` right after the backfill to close the desync window (its views
reference the re-derived identifiers).

### Misc

- `people-data-labs` `1.0.7`: views temporarily pin the new derived
identifiers (TODO: import from the next released `twenty-sdk`).
- `UpgradeStatusModule` split out of `UpgradeModule` so the application
module cluster can consume upgrade status/migration services without
importing the versioned command bundles (fixes a require cycle that
crashed boot).
- Docs: `system-fields.mdx` documents the system relation fields and
their resolver; `sync-and-recovery.mdx` plan example no longer shows
auto-injected relations.

## Known red CI

`people-data-labs (dockerhub-latest)` fails by design until the 2.23
server image is published: the app pins the new identifiers which only
exist on a 2.23 server. The `local` leg (server built from this branch)
is green.

## System fields are no longer manifest-authorable (accepted regression)

The manifest converter no longer derives `isSystem` /
`isSystemSideEffect` from field names. Reserved-system-named manifest
fields (`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`,
`updatedBy`, `position`, `searchVector`) are now skipped at conversion
time when they carry the exact derived universal identifier (keeps
manifests built with older SDKs installable), and rejected with
`INVALID_INPUT` when they pin any other identifier. System fields are
therefore fully engine-canonical: nothing a manifest carries can produce
a system-flagged entity anymore.

**Accepted regression**: a manifest can no longer influence system field
properties at all. Previously a (legacy) re-declaration could shape them
at creation — which actually produced broken system fields, e.g. a
nullable, non-unique `id` — and could still toggle the allowlisted
`isActive` / `universalSettings` afterwards. We consider this acceptable
for now: per-app granularity over system fields will be reintroduced
later through the **override framework**, which will also settle update
semantics by forbidding direct updates over `isSystemSideEffect: true`
entities and expressing divergence as overrides.

`isSystemSideEffect`-only entities (the default relation fields
provisioned by this PR) still have no engine-level update guard (see
Follow-up below); that part is unchanged and also lands with the
overrides refactor.

## Follow-up

`isSystemSideEffect` field update/delete guards intentionally live at
the API layer (`sanitize-raw-update-field-input.ts`,
`from-delete-field-input-...util.ts`) rather than in the engine-level
`FlatFieldMetadataValidatorService`. Moving them into the validator
requires threading operation-origin (direct field mutation vs engine
cascade) through the migration matrix, otherwise legitimate object
rename/delete cascades (which carry `isSystemBuild=false`) would be
rejected. Tracked in twentyhq/core-team-issues#2671.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22882?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-20 18:53:24 +02:00
martmull 1b5e974629 feat(call-recorder): add copy-to-clipboard buttons for transcript, summary, and video link (#23052)
## Context

Closes twentyhq/core-team-issues#2692.

Adds copy-to-clipboard actions to the call recorder app so users can
quickly share a call's transcript, summary, and video.

## What changed

- **Copy transcript** button in the *Recording and Transcript* widget
header. Copies the transcript as plain text with resolved speaker
display names and timestamps (mirroring what is shown on screen).
- **Copy video download link** button in the same header. Copies the
signed video file URL.
- **Copy summary** button in the *Summary* widget header. Copies the
summary markdown.

Each button is powered by a new reusable `CopyToClipboardButton`
component that writes to the clipboard, briefly swaps to a check icon
for feedback, and surfaces a success/error snackbar. Buttons are
disabled when there is nothing to copy (no transcript / video / summary,
or while loading).

A `buildTranscriptPlainText` utility turns parsed transcript entries
into shareable text, with participant display names preferred over raw
diarized speaker labels.

## Screenshots

The *Recording and Transcript* header now shows a copy-transcript and a
copy-video-link button, and the *Summary* header shows a copy-summary
button.

| Light | Dark |
| --- | --- |
| <img width="426"
src="https://raw.githubusercontent.com/twentyhq/twenty/claude/issue-2692-screenshots/.github/pr-screenshots/2692/call-recorder-copy-buttons-light.png"
/> | <img width="426"
src="https://raw.githubusercontent.com/twentyhq/twenty/claude/issue-2692-screenshots/.github/pr-screenshots/2692/call-recorder-copy-buttons-dark.png"
/> |

## Tests

- New unit tests for `buildTranscriptPlainText` (speaker/timestamp
formatting, missing timestamps, participant name resolution).
- Full app unit suite passes (491 tests), plus typecheck and lint.
2026-07-20 15:40:17 +00:00
Etienne 5bf3472eb9 chore(twenty-exa): bump to 0.2.0, add marketplace metadata and Twenty version floor (#23063)
## What

Prepares the Exa app (`@twentyhq/twenty-exa`) for a fresh npm release.

- Bump `version` `0.1.0` → `0.2.0`
- Add `engines.twenty: ">=2.19.0"` so older servers don't install an
incompatible build
- Add marketplace metadata in `defineApplication()`: `category:
'Search'`, `websiteUrl`, `termsUrl`, `emailSupport`, `issueReportUrl`
(matching the values used by the other `@twentyhq/*` apps)

## Why

The version currently published on npm is the **unscoped**
`twenty-exa@0.1.0`, which predates several SDK breaking changes. The
in-repo source has since migrated to `twenty-sdk@~2.16` and `exa-js` v2:

- `chargeCredits` now imported from `twenty-sdk/billing` (was a local
util)
- logic function uses `toolTriggerSettings.inputSchema` (was `isTool` +
`toolInputSchema`)
- schema type imported from `twenty-sdk/logic-function` (was
`twenty-shared/logic-function`)
- `category` enum updated to the exa-js v2 union (removed
`github`/`tweet`/`linkedin profile`, added `people`)

So the published build is effectively broken on current servers. This PR
readies a `0.2.0` release under the standard scoped name
`@twentyhq/twenty-exa`.

The app's `universalIdentifier` is unchanged
(`2b7f4a2e-9c4b-4a11-b63c-2e5e7d3f5a9a`), so Twenty treats this as the
**same app** and upgrades existing installs in place — the name change
(unscoped → scoped) is only an npm-registry concern.

## Changes

- `packages/twenty-apps/public/twenty-exa/package.json`
- `packages/twenty-apps/public/twenty-exa/src/application.config.ts`

## Testing

- `yarn typecheck` — pass
- `yarn lint` — pass (0 errors)
- `yarn twenty dev:build` — builds a valid `@twentyhq/twenty-exa@0.2.0`
tarball

## Follow-up (not in this PR — npm/ops, needs auth)

- Publish `@twentyhq/twenty-exa@0.2.0` to npm (`yarn twenty
app:publish`)
- Deprecate + de-keyword the old unscoped `twenty-exa` so only one
package feeds the shared `universalIdentifier` on catalog sync

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23063?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-20 13:49:39 +00:00
Rashad Karanouh e6c6cccafa v1.3.0 — Partner workspace self-service (glowup app) (#22929)
## Glowup — app · v1.3.0 (Release ② of the brief + glowup rollout)

Partner **workspace self-service**: partners manage their own profile,
links, services, and case studies from inside the CRM (new objects +
record-page views + a "My Profile" self-service front-component).
Evolved superset of the closed #22470 (v1.3.0). App-only — **0 website
files**.

Version **1.3.0** (prod is currently 1.2.10). SDK **2.19.0**.

Supersedes **#22470** (closed).

### Verified locally
Provisioned a throwaway workspace, synced the schema, seeded, and
exercised the full surface end-to-end: marketplace + public profiles
render live; **partner self-service pages** (My Profile / My Case
Studies / links / services) load and save when acting as a partner user;
both intake forms (partner application + client brief) submit
successfully. `oxlint` 0/0, typecheck clean.

### Notes
- Committed `APPLICATION_UNIVERSAL_IDENTIFIER` is the **canonical** prod
id `e662fc1f-02c1-41ff-b8ba-c95a447b3965` (local bundle rewrites it to a
throwaway that stays uncommitted).
- New views reference app-owned fields only — no hardcoded system-field
ids.

### Remaining before merge
- CI lint / typecheck / tests (green locally).
- Refresh the partners-doc (new objects/views change the app surface).

---

## 🚦 Release order — do not break

```
① BRIEF WEB — #22291   MERGED (website deploy pending prod CLIENT_BRIEF_* env vars)
        │
        ▼
② GLOWUP APP — THIS PR  (rk-partner-profile-page v1.3.0 → main)   ⟵ replaces #22470
   merge → DEPLOY TO PROD  (verify canonical id first,
                            yarn twenty deploy && install -r partner-twenty-com)
         → set new app variables on prod → refresh partners-doc
        │   ⟵⟵ GATE for ③ ⟵⟵
        ▼
③ GLOWUP WEB — rk-glowup-web-stacked  (reopen ONE PR, base main; was #22471 / #22402)
   ONLY after ② is LIVE on prod (the site reads the new links / services / case-study objects)
```

- ② gates only ③. After ② deploys, reconcile **#22637**
(partners-traffic-web) with ③ — both touch `partners-marketplace/*`.
2026-07-20 15:25:07 +02:00