Commit Graph
991 Commits
Author SHA1 Message Date
neo773andGitHub d040756fcf remove direction from messages (#20026)
This was a leftover column removed in
https://github.com/twentyhq/twenty/pull/6743 but was accidentally added
again when we migrated to `buildMessageStandardFlatFieldMetadatas` from
workspace decorator

/closes #20011
2026-05-05 15:24:25 +02:00
53fdac1417 feat(apps): split AI tool and workflow action triggers in LogicFunction manifest (#20208)
## Summary

Replaces the bolted-on `isTool` + `toolInputSchema` fields on
`LogicFunctionManifest` with two distinct, opt-in triggers that align
with the existing `cron` / `databaseEvent` / `httpRoute` trigger
pattern:

- **`toolTriggerSettings`** — exposes the function as an AI tool (chat /
MCP / function calling). Uses standard JSON Schema (the format LLMs
natively understand).
- **`workflowActionTriggerSettings`** — exposes the function as a step
in the visual workflow builder. Uses Twenty's rich `InputSchema` so the
builder can render proper `FieldMetadataType`-aware editors, variable
pickers, labels, and an optional `outputSchema`.

A function can opt into none, one, or both. Each surface gets the schema
format appropriate for it.

### Why

`isTool: true` previously exposed the function as both an AI tool AND a
workflow node, with the same JSON Schema feeding both — but the workflow
builder really wants Twenty's `InputSchema` (with `CURRENCY`,
`RELATION`, `EMAILS`, etc.) and the AI surface really wants standard
JSON Schema. Today the workflow builder hacks around this by treating
JSON Schema as `InputSchema`, which silently breaks for any
non-primitive field type. Splitting the triggers fixes that and lets
each surface evolve independently.

### Migration

- **Fast** instance command adds the two new nullable columns.
- **Slow** instance command backfills `toolTriggerSettings` +
`workflowActionTriggerSettings` from `isTool=true` rows (preserving
today's both-surfaces behaviour) then drops the legacy columns.

### Stacked

Stacked on top of #20181. Merge that first, then this.

## Test plan

- [ ] CI green (oxlint, typecheck, jest, vitest)
- [ ] Run `--include-slow` upgrade against a workspace with existing
`isTool=true` logic functions; verify both new columns populated and old
columns dropped
- [ ] Verify AI chat sees migrated tool functions (Linear create-issue,
Exa search) and can call them with the JSON Schema
- [ ] Add an AI-tool function from the Settings UI (toggles
`toolTriggerSettings`) and verify it shows up in chat
- [ ] Add a workflow-action function from the Settings UI (toggles
`workflowActionTriggerSettings`) and verify it appears in the workflow
node picker
- [ ] In the workflow builder, edit a `LOGIC_FUNCTION` step and verify
input fields render (no more JSON-Schema-as-InputSchema hack)
- [ ] Try defining a function with no triggers in the SDK and verify
`defineLogicFunction` rejects it

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: martmull <martmull@hotmail.fr>
2026-05-05 14:56:09 +02:00
e3be1f4971 Make ConnectionProvider a true SyncableEntity (#20232)
## Summary

PR #20181 left `ConnectionProvider` in the `SyncableEntity` enum but
bypassing the standard sync pipeline — manifest sync called the bespoke
`ApplicationOAuthProviderService.upsertManyFromManifest()` instead of
going through the workspace-migration orchestrator like every other
SyncableEntity. Anything that assumed *"all SyncableEntity values flow
through the same pipeline"* (dev UI sync tracking, verification tooling)
was wrong about ConnectionProvider — that's the inconsistency this PR
closes.

This PR follows the `.cursor/skills/syncable-entity-*` guides
religiously, all six steps.

## What changes

**Step 1 — Types & Constants** (`@syncable-entity-types-and-constants`)
- Add `connectionProvider` to `ALL_METADATA_NAME` (twenty-shared)
- Make `ApplicationOAuthProviderEntity` extend `SyncableEntity` (drops
the ad-hoc columns since the base class provides them, adds `deletedAt`,
drops the old `(applicationId, universalIdentifier)` unique in favour of
SyncableEntity's `(workspaceId, universalIdentifier)`)
- `FlatConnectionProvider`, `FlatConnectionProviderMaps`,
`FLAT_CONNECTION_PROVIDER_EDITABLE_PROPERTIES`,
`UniversalFlatConnectionProvider`, six action types
- Register in **all** the central registries:
`AllFlatEntityTypesByMetadataName`,
`ALL_METADATA_ENTITY_BY_METADATA_NAME`,
`ALL_ENTITY_PROPERTIES_CONFIGURATION`, `ALL_MANY_TO_ONE_*`,
`ALL_ONE_TO_MANY_*`, `ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION`,
`ALL_METADATA_SERIALIZED_RELATION`,
`ALL_JSONB_PROPERTIES_WITH_SERIALIZED_RELATION`,
`WORKSPACE_CACHE_KEYS_V2` (`flatConnectionProviderMaps`),
`METADATA_EVENTS_TO_EMIT`
- `case 'connectionProvider':` in seven discriminated-union switches
(`derive-metadata-events-*`, `optimistically-apply-*`,
`enrich-create-*`)

**Step 2 — Cache & Transform** (`@syncable-entity-cache-and-transform`)
- `WorkspaceFlatConnectionProviderMapCacheService` (extends
`WorkspaceCacheProvider`, decorated with `@WorkspaceCache`,
soft-delete-aware)
- `fromConnectionProviderEntityToFlatConnectionProvider` util
- `fromConnectionProviderManifestToUniversalFlatConnectionProvider` util
- `FlatConnectionProviderModule` wires the cache service
- Wired the manifest converter into
`compute-application-manifest-all-universal-flat-entity-maps`

**Step 3 — Builder & Validation**
(`@syncable-entity-builder-and-validation`)
- `FlatConnectionProviderValidatorService` — never throws, returns error
arrays; uses indexed `byUniversalIdentifier` for the (name,
applicationUniversalIdentifier) uniqueness check (no
`Object.values().find()` on the hot path)
- `WorkspaceMigrationConnectionProviderActionsBuilderService`
- Registered in both validators-module + builder-module
- **Wired into the orchestrator** (the most-commonly-forgotten step per
the rule) — constructor inject, destructure
`flatConnectionProviderMaps`, `validateAndBuild`, append actions to the
final migration

**Step 4 — Runner & Actions** (`@syncable-entity-runner-and-actions`)
- Three handlers (create / update / delete) using the canonical
`WorkspaceMigrationRunnerActionHandler` mixin
- Registered in `WorkspaceSchemaMigrationRunnerActionHandlersModule`

**Step 5 — Integration** (`@syncable-entity-integration`)
- Delete the `upsertManyFromManifest` bypass on
`ApplicationOAuthProviderService`
- Remove the bypass call from `ApplicationSyncService` — manifest sync
now flows through the standard pipeline
- Drop `ApplicationOAuthProviderModule` from `ApplicationManifestModule`
(no longer needed)
- Import `FlatConnectionProviderModule` from
`ApplicationOAuthProviderModule` to keep the cache discoverable
- 3 new exception codes: `INVALID_CONNECTION_PROVIDER_INPUT`,
`CONNECTION_PROVIDER_NOT_FOUND`,
`CONNECTION_PROVIDER_NAME_ALREADY_EXISTS`

**Migration**
- Generated via `database:migrate:generate` (instance command
`1777896012579`): drops the old `(applicationId, universalIdentifier)`
unique constraint, adds `deletedAt` column, adds the `(workspaceId,
universalIdentifier)` unique index that `SyncableEntity` requires.
- Verified clean — a second `migrate:generate` pass produces zero drift.

**Step 6 — Tests** (`@syncable-entity-testing`)
- 3 new specs for the manifest converter (defaults, optional fields,
all-fields)
- All 32 existing OAuth-provider tests still pass
- ConnectionProvider has no end-user GraphQL CRUD (it's manifest-driven
only), so the GraphQL integration suite that other SyncableEntities ship
doesn't apply here

**Codegen**
- Regenerated GraphQL artifacts (twenty-front + twenty-client-sdk)
against the live schema

## Why this matters

Before:
- `ConnectionProvider` claimed to be a `SyncableEntity` (in the enum)
- But the entity didn't extend `SyncableEntity`
- And the manifest sync bypassed the standard pipeline
- → Verification tooling, dev UI sync tracking, anything iterating over
`ALL_METADATA_NAME` got inconsistent behaviour

After:
- `ConnectionProvider` is a `SyncableEntity` end-to-end
- Single sync path through the workspace-migration orchestrator (same as
`agent`, `skill`, `frontComponent`, `webhook`, …)
- One mental model

## Out of scope (deliberate)

- **Renaming the table** from `applicationOAuthProvider` to
`connectionProvider` — the `metadataName` is `connectionProvider` (what
consumers see in code); the table name is internal. A rename would
balloon this PR with mechanical churn unrelated to the sync-pipeline
wiring. Worth doing as a follow-up.
- **`applicationVariable` SyncableEntity conversion** — the other
manifest-sync holdout. Tracked in #20215.

## Test plan

- [ ] Migration up/down clean against fresh DB
- [ ] Install an app whose manifest declares connection providers —
providers appear in the workspace
- [ ] Re-deploy the app with one provider added, one removed, one
renamed → all reconciled correctly via the sync pipeline
- [ ] Verify the dev-UI sync-tracking page shows ConnectionProvider
entries the same way it shows agents/skills/etc
- [ ] OAuth flow still works (existing connections, new connections,
reconnect, list/get from SDK) — should be unchanged since the runtime
code path didn't move

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 14:04:15 +02:00
e6399b180e Fix/workspace member avatars 20193 (#20200)
Fixes #20193

**Bug Description:**
Previously, workspace member avatars failed to render correctly in table
views and relation chips (such as the Account Owner field). While the
avatar picker dropdown correctly fetched fresh GraphQL data, table views
and chips relied on the cached defaultAvatarUrl or avatarUrl fields,
which were frequently resolving to empty strings or failing to parse
external OAuth URLs correctly.

**Root Cause:**

- Empty String Defaults: Deleting an avatar or failing to retrieve one
defaulted the database state to an empty string ("") instead of null,
which caused frontend image components to break rather than render their
fallback states.

- Missing Permanent URLs: The WorkspaceMemberTranspiler was strictly
expecting internal signed URLs. If an avatar was an external OAuth URL,
it incorrectly returned an empty string, breaking SSO profile pictures.

- Missing Fallbacks: New users lacked a proper Gravatar fallback
assignment upon workspace creation.

**Changes Made:**

- user-workspace.service.ts: Updated the avatar computation logic during
user creation to implement a reliable Gravatar fallback and correctly
set missing avatars to null instead of empty strings. Updated the
storage to use permanent file URLs.
- file-url.service.ts: Implemented a getRawFileUrl method to support
rendering permanent, non-expiring file URLs for avatars.
- workspace-member-transpiler.service.ts: Refactored the URL
transpilation logic to gracefully pass through external OAuth URLs
(e.g., Google/Microsoft profile pictures) instead of stripping them.
- WorkspaceMemberPictureUploader.tsx: Fixed the frontend removal logic
so that deleting a profile picture sets the avatarUrl to null
(consistent with the backend) rather than an empty string.

**Testing:**

- Verified that avatars correctly display in relation chips and table
views.
- Verified that external OAuth avatars load properly.
- Verified that deleting an avatar correctly resets the UI to the
fallback initials component.

Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 19:42:12 +00:00
MarieandGitHub 4852ac401a Add server upgrade status on admin panel (#20107)
## Summary

Adds an admin upgrade-status panel that surfaces per-instance and
per-workspace migration health, backed by a Redis-cached aggregate to
keep the page snappy on large fleets.

<img width="827" height="880" alt="Screenshot 2026-04-28 at 10 21 03"
src="https://github.com/user-attachments/assets/8f88baa9-7268-4eff-bf6a-906a7f06ca91"
/>
<img width="804" height="892" alt="Screenshot 2026-04-28 at 10 21 11"
src="https://github.com/user-attachments/assets/1e6decf8-766a-4d0e-96b1-03a9962bba3c"
/>


## Computed metrics

**Instance** (`InstanceUpgradeStatus`)
- `inferredVersion` — version derived from the latest non-initial
instance command name
- `health` — `upToDate` | `behind` | `failed`, derived from the latest
attempt vs. the last expected instance step in the upgrade sequence
- `latestCommand` — `{ name, status, executedByVersion, errorMessage,
createdAt }` from the most recent attempt

**Per-workspace** (`WorkspaceUpgradeStatus`)
- `workspaceId`, `displayName`
- `inferredVersion`, `health`, `latestCommand` (same shape as instance),
computed against the latest expected step in the sequence

**Aggregate** (`AllWorkspacesUpgradeStatus`, only across `ACTIVE` /
`SUSPENDED` workspaces)
- `instanceUpgradeStatus`
- `totalCount`, `upToDateCount`, `behindCount`, `failedCount`
- `workspacesBehindIds[]`, `workspacesFailedIds[]`
- `computedAt`

## Fetching strategy

All reads go through `UpgradeStatusCacheService` (cache namespace:
`EngineHealth`).

- **Aggregate read** (`getAllWorkspacesStatus` →
`getAllWorkspacesUpgradeStatus` query):
reads summary + behind-ids + failed-ids in parallel; if any of the three
keys is missing, full recompute (`recomputeAllWorkspaces`) is triggered,
which also primes per-workspace entries.
- **Per-workspace read** (`getWorkspacesStatus(ids)` →
`getUpgradeStatus(ids)` query):
`mget` on workspace keys; misses are recomputed individually
(`recomputeWorkspace`), and aggregates are reconciled in place (count +
id list deltas) without a full recompute.
- **Recompute on demand**: `refreshUpgradeStatus` mutation calls
`recomputeAllWorkspaces` to bypass cache and rewrite all keys.
- **Auto-invalidation**: `InstanceCommandRunnerService` (fast + slow
paths) and `WorkspaceCommandRunnerService` invalidate after every run
via `safeInvalidateUpgradeStatusCache()`
(`flushByPattern('upgrade-status:*')`). Failures in cache invalidation
are swallowed and logged so they never break the migration runner.
- **TTL**: `60 * 60 * 1000` ms (1 hour) on every key — protects against
stale data even if a runner crashes before invalidating.

## Introduced cache keys

All under the `EngineHealth` cache-storage namespace:

| Key | Type | Purpose |
| --- | --- | --- |
| `upgrade-status:all-workspaces:summary` |
`CachedAllWorkspacesStatusSummary` | Counts + instance status +
`computedAt` |
| `upgrade-status:all-workspaces:behind-ids` | `string[]` | Workspace
ids in `behind` state |
| `upgrade-status:all-workspaces:failed-ids` | `string[]` | Workspace
ids in `failed` state |
| `upgrade-status:workspace:<workspaceId>` |
`CachedWorkspaceUpgradeStatus` | Per-workspace status (one key per
workspace) |

Full invalidation uses the pattern `upgrade-status:*`.

## Index added on `upgradeMigration` (already added on prod)

Migration
`2-2-instance-command-fast-1777308014234-addUpgradeMigrationWorkspaceIdIndex.ts`:

```sql
CREATE INDEX "IDX_upgradeMigration_workspaceId_name_attempt"
  ON "core"."upgradeMigration" ("workspaceId", "name", "attempt")
  WHERE "workspaceId" IS NOT NULL;
2026-05-04 15:06:07 +00:00
9e94045fa5 feat(apps): generic OAuth provider support for app SDK (#20181)
## Summary

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

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

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

## Architecture

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

## Reference app

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

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

## Tests

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

## Test plan

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

## Out of scope (deliberately)

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

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

---------

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

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

## What changed

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

## Why this is safe

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

## Context

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

## Test plan

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

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

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-01 07:51:21 +02:00
WeikoandGitHub 85752f8a61 Bump 2.3.0 (#20169) 2026-04-30 19:34:43 +02:00
nitinandGitHub e1828b6f41 [AI] Add thread actions, filters, and archive support (#20068)
## PR Description

### Summary
- Add AI chat thread actions: rename, archive (soft-delete via
`deletedAt`), and hard-delete with confirmation.
- Add chat thread filtering by status (active/archived/all), group-by
mode, and last activity.
- Rework drawer/side-panel thread lists to share thread sections, item
menus, archive icons, and empty-state behavior.
- Extend server chat thread model/API with `deletedAt`, mutations,
broadcasts, and archive-aware stream guards.

### Decisions
- Two-stage lifecycle: Archive sets `deletedAt` (soft); Delete is a
separate action on archived threads that hard-deletes the row. Aligns
with Twenty's soft-delete convention (Felix's suggestion).
- `lastMessageAt` is derived from `MAX(agentMessage.createdAt)` on read,
not stored. List query does inline aggregation for sort; `@ResolveField`
covers single-thread / mutation paths so the schema contract is honest
everywhere. Matches `timeline-messaging.service.ts` precedent and the
existing `totalInputCredits` / `totalOutputCredits` `@ResolveField`
pattern in the same resolver.
- Replaced auto-CRUD `chatThreads` (cursor-paginated Connection) with a
custom `[AgentChatThreadDTO!]` resolver. Frontend metadata-store treats
threads as a flat collection and filters/sorts client-side, so cursor
pagination was performative.
- Sending in an archived chat unarchives it optimistically on the client
and authoritatively on the server.
- Grouping and last-activity filtering use `lastMessageAt ?? updatedAt`
so archive/rename don't bump threads in the list.
- Kept metadata-store core API unchanged; AI chat uses the same local
cast pattern already used by other metadata-store partial updates.


https://github.com/user-attachments/assets/1b179b7b-1a2a-4a7a-aa0a-c88f6f051a87
2026-04-30 15:42:10 +00:00
martmullandGitHub bddd23fd9c Fix application icons (#20142)
fixes application chip (icon Name) in all setting tables

## After
<img width="1200" height="896" alt="image"
src="https://github.com/user-attachments/assets/bd377f47-1d52-4142-b904-f2ce90c1db78"
/>
<img width="1200" height="917" alt="image"
src="https://github.com/user-attachments/assets/f49cc742-f11e-47e3-86ed-34beffe493c7"
/>
<img width="1234" height="878" alt="image"
src="https://github.com/user-attachments/assets/2ab459de-5f9d-4d39-9490-eec4ed9ee432"
/>
<img width="1239" height="845" alt="image"
src="https://github.com/user-attachments/assets/3c1bf258-285a-47b9-a60d-05ba1564334d"
/>
<img width="1183" height="907" alt="image"
src="https://github.com/user-attachments/assets/715b2470-2d88-48e3-88ac-d3daf3451717"
/>
<img width="1300" height="912" alt="image"
src="https://github.com/user-attachments/assets/d7c829fa-bf1d-4f19-82de-a8bf29e22bfa"
/>
2026-04-30 09:32:04 +00:00
EtienneandGitHub fd6d5f895d Ai Chat - Caching optim (#20126)
EDIT : 
- solving auto-caching from Anthropic by updating ai-sdk/anthropic +
adding providerOption at stream level
- concerning Bedrock, it needs breakpoint


**1. Breakpoints were only on the system prompt**

The code already placed a cache marker on the system prompt (~10K
tokens). But the conversation history — which can grow to hundreds of
thousands of tokens — had no marker, so Anthropic re-read it at full
price on every turn.

The fix adds a prepareStep hook inside streamText that stamps the last
message with a cache breakpoint before every LLM call. Anthropic then
caches the entire conversation prefix, and subsequent turns read it at
$0.30/M instead of $3/M.

prepareStep is used rather than a one-shot pre-processing step because
an agentic turn makes multiple internal LLM calls as tool results
accumulate — the hook refreshes the breakpoint before each one.

**2. Bedrock was using the wrong field**

The system prompt marker for Bedrock was set as cacheControl: { type:
'ephemeral' } — which is the Anthropic wire format. The Bedrock Converse
API expects cachePoint: { type: 'default' }. The system prompt was
silently not being cached on Bedrock at all.

Both the system prompt and the new prepareStep now go through a shared
getCacheProviderOptions helper that returns the correct field per
provider.

**3. Persisted cached token usage to monitor cache strat. efficiency**
2026-04-29 14:42:12 +00:00
11628d19a3 add recurring calendar events for google cal (#19748)
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-29 14:06:57 +00:00
Paul RastoinandGitHub b49e58dfc6 Fix click house migration (#20127)
`it does not support renaming of multiple tables in single query.`

@etiennejouan manually fix the corrupted clickhouse instance
2026-04-29 10:48:21 +00:00
Paul RastoinandGitHub d2dda67596 Fix upgrade --start-from-workspace-id (#20116)
# Introduction
Prevent using both `--start-from-workspace-id` and `--workspace`

When any of the two are being passed we prevent passing to the next
instance segment, it would require an upgrade re run even if legit

When `--start-from-workspace-id` is passed we filter from all the
fetched active or suspended workspace ids and apply equivalent filter as
before
2026-04-28 16:46:15 +00:00
EtienneandGitHub fbaea0639a Billing - optimize usageEvent CH table (#20019)
- Update usageEvent clickhouse table, partitioning, indexing and
projection (auto materialized view) to optimize credit usage queries
- Add caching for available credits and billing subscription


To do in next PR: deprecate enforceCapUsage cron. Bonus : real-time on
billingSubscription
2026-04-28 14:06:49 +00:00
Charles BochetandGitHub d74e3fa3b5 chore(server): bump current version to 2.2.0 (#20040)
## Summary

We are releasing Twenty v2.2.0. This PR sets up the
upgrade-version-command machinery for the new release line:

- Promote `2.1.0` into `TWENTY_PREVIOUS_VERSIONS` (it just shipped)
- Set `TWENTY_CURRENT_VERSION` to `2.2.0`
- Reset `TWENTY_NEXT_VERSIONS` to `[]`
- Refresh the `InstanceCommandGenerationService` snapshots to reflect
the new current version (`2.2.0` / `2-2-` slug)
- Update the failing-sequence-runner snapshot to include `2.2.0` in the
covered versions list

The `2-2/` upgrade-version-command module is already in place and wired
into `WorkspaceCommandProviderModule`, so future upgrade commands
targeting `2.2.0` can land directly under `2-2/` (or be generated
against `--version 2.2.0`).
2026-04-24 22:19:16 +02:00
Charles BochetandGitHub 80c0e5603d Move isPreInstalled applicationRegistration instance command to 2.1 (#20037)
## Summary

The fast instance command adding `isPreInstalled` to
`core.applicationRegistration` was added after 2.0 was released, so it
must run as part of the 2.1 upgrade rather than 2.0.

- Renamed
`2-0/2-0-instance-command-fast-1776886452831-add-is-pre-installed-to-application-registration.ts`
to
`2-1/2-1-instance-command-fast-1776886452831-add-is-pre-installed-to-application-registration.ts`
- Updated `@RegisteredInstanceCommand` version from `'2.0.0'` to
`'2.1.0'`
- Updated the import path in `instance-commands.constant.ts`

The original timestamp (`1776886452831`) was kept; it is smaller than
the existing 2.1 fast command timestamp, so this command will simply run
first within the 2.1.0 batch.
2026-04-24 17:52:58 +00:00
WeikoandGitHub 2ccc293f99 Gate export/import command menu items by permission flag (#19991)
## Summary
- Hides the `exportRecords`, `exportView`, and `importRecords` command
menu actions from
users whose role does not hold the matching `EXPORT_CSV` / `IMPORT_CSV`
permission flag.
- Exposes the current user's role permission flags to
`conditionalAvailabilityExpression`
by adding `permissionFlags: Record<string, boolean>` to
`CommandMenuContextApi`, mirroring
 how `featureFlags` is already accessible.
- Adds a `2.1.0` workspace upgrade command that rewrites the three
existing rows on every
 active/suspended workspace.

## Before
<img width="1294" height="287" alt="Screenshot 2026-04-22 at 19 37 40"
src="https://github.com/user-attachments/assets/11ca8635-14d7-40a0-9ca0-76329c54e3c6"
/>

## After
<img width="1283" height="285" alt="Screenshot 2026-04-22 at 19 32 25"
src="https://github.com/user-attachments/assets/5e49fa8a-4541-42ee-96da-4c1de7d00aae"
/>
2026-04-24 15:22:44 +00:00
251f5deab6 [breaking, deploy server first] fix(ai-chat): persist providerExecuted flag on tool parts (#20030)
## Summary
Fixes Sentry errors of the form:

> \`messages.3: \`tool_use\` ids were found without \`tool_result\`
blocks immediately after: srvtoolu_…. Each \`tool_use\` block must have
a corresponding \`tool_result\` block in the next message.\`

### Root cause
When the model invokes a **provider-hosted tool** (e.g. Anthropic's
native \`web_search\` — note the \`srvtoolu_\` ID prefix), the AI SDK
marks the resulting \`UIMessagePart\` with \`providerExecuted: true\`.
\`convertToModelMessages\` uses that flag to emit the
tool_use/tool_result pair *inside the same assistant message* — the
format Anthropic requires for server-side tools.

Our \`AgentMessagePart\` persistence was dropping \`providerExecuted\`
on the way to the DB (and re-hydration didn't know to set it). On the
next turn, \`convertToModelMessages\` treated the rehydrated part as a
client-side tool call, splitting it into \`assistant(tool_use)\` +
\`user(tool_result)\` — which Anthropic then rejects with the error
above.

### Fix
- Add nullable \`providerExecuted BOOLEAN\` column on
\`core.agentMessagePart\` via a fast instance command.
- Surface the field on \`AgentMessagePartDTO\` (GraphQL).
- Preserve it through \`mapUIMessagePartsToDBParts\` (server) and both
\`mapDBPartToUIMessagePart\` mappers (server + frontend).
- Include it in \`GET_CHAT_MESSAGES\` and \`GET_AGENT_TURNS\`
selections.
- Regenerate \`generated-metadata/graphql.ts\`.

### Backwards compatibility
Existing rows have \`NULL providerExecuted\` and round-trip as the
omitted flag — which is exactly the pre-fix behaviour for tool parts
that were never provider-executed. Only *new* assistant messages using
\`web_search\` (or other provider-hosted tools) will write \`true\`, and
those are the only ones that were breaking.

## Test plan
- [x] \`npx tsgo\` typecheck — server + front clean
- [x] \`oxlint\` + \`prettier --check\` on all touched files — clean
- [x] \`npx nx run twenty-server:database:migrate:prod\` runs the new
instance command locally; \`providerExecuted\` column present on
\`core.agentMessagePart\`
- [x] Regenerated \`generated-metadata/graphql.ts\` —
\`providerExecuted\` wired into both queries and \`AgentMessagePart\`
type
- [ ] Manual: start a chat with Anthropic web_search enabled, invoke the
tool in turn 1, reply in turn 2 — should not throw the srvtoolu error

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:58:26 +02:00
WeikoandGitHub 5f604a503a backfill widget position from gridPosition (phase 1 of gridPosition removal) (#20032)
## Context
Phase 1 of removing the legacy `gridPosition` field from
`PageLayoutWidget` in favor of the new `position` discriminated union
(`grid` / `vertical-list` / `canvas`). This PR is purely additive —
`gridPosition` is still required and read everywhere; we just guarantee
that every widget now also has a non-null `position` so a follow-up PR
can drop `gridPosition` cleanly.

## Changes
- **Slow instance command**
`BackfillPageLayoutWidgetPositionSlowInstanceCommand` (2.1.0):
for every `core.pageLayoutWidget` row where `position IS NULL`, copies
`gridPosition`
into `position` with `layoutMode: 'GRID'`. Historically only grid
widgets used
`gridPosition`, so a single SQL update covers every existing row.
- **`PageLayoutDuplicationService`**: when duplicating a widget, also
forwards
`originalWidget.position` (was previously only forwarding
`gridPosition`).
- **Frontend default layouts** (10 `Default*PageLayout.ts` files): added
a `position`
sibling to every widget, matching the parent tab's `layoutMode` —
`VERTICAL_LIST` widgets
get `{ layoutMode, index }`, `CANVAS` widgets get `{ layoutMode }`

<img width="338" height="226" alt="Screenshot 2026-04-24 at 16 23 17"
src="https://github.com/user-attachments/assets/c3319318-f1b8-4271-96b4-196b209a1f5e"
/>
2026-04-24 14:35:56 +00:00
nitinandGitHub 097432d3a2 [Command Menu] Refactor layout customization conditional availability [Warning] (#19974)
closes
https://discord.com/channels/1130383047699738754/1494312529286004837
2026-04-24 10:18:02 +00:00
4f938aa097 feat(app): infrastructure for pre-installed apps (#19973)
**PR 1 of 2.** Follow-up PR ships the Exa app, sets it as a default
pre-installed app, and removes the current `WebSearchTool` /
`WebSearchService` / `ExaDriver`. This PR adds the plumbing; no
user-visible change yet.

## Summary

- Server admins can declare a list of npm app packages to auto-install
on every new workspace and backfill onto existing workspaces via CLI.
- Server-level secrets (like Exa's API key) live on the
`ApplicationRegistration` (one row per server, encrypted) and are
injected into logic function execution env at runtime. No more
per-workspace storage of global secrets.
- A generic `POST /app/billing/charge` endpoint lets app logic functions
emit workspace usage events for metered features. Exa uses it in PR 2;
future apps (call recorder, etc.) reuse it.
- `LogicFunctionToolProvider` tool name prefix changes `logic_function_`
→ `app_`. Shorter, accurate (they come from installed apps).

## What's in this PR

**Logic function executor — server-level variables**
- `LogicFunctionExecutorService.getExecutionEnvVariables` now resolves
env vars in the order: hardcoded defaults →
`ApplicationRegistrationVariable[]` (server-level) →
`ApplicationVariable[]` (workspace-level override). The manifest
`serverVariables` schema has existed; this closes the loop.

**Config**
- `PRE_INSTALLED_APPS` — comma-separated list of npm packages. Default:
empty.

**\`PreInstalledAppsService\`** (new module)
- \`onApplicationBootstrap()\` — fetches each package's manifest from
the app registry CDN, upserts an \`ApplicationRegistration\`, and seeds
declared \`serverVariables\` from matching env vars (e.g.
\`EXA_API_KEY\` env → encrypted registration variable).
- \`installOnWorkspace(workspaceId)\` — installs all pre-installed apps
on a single workspace. Tolerates per-app failures.

**Auto-install on new workspace activation**
- \`WorkspaceService.prefillCreatedWorkspaceRecords\` invokes
\`installOnWorkspace\` after prefilling standard records. Non-blocking
on failure.

**Backfill CLI command**
- \`install-pre-installed-apps\` — iterates active and suspended
workspaces, installs pre-installed apps that aren't yet installed.
Idempotent. Run after changing \`PRE_INSTALLED_APPS\`.

**App billing endpoint**
- \`POST /app/billing/charge\`. Authenticated via \`APPLICATION_ACCESS\`
token (already injected into logic function execution env as
\`DEFAULT_APP_ACCESS_TOKEN\`). Body: \`{ creditsUsedMicro, quantity,
unit, operationType, resourceContext? }\`. Emits \`USAGE_RECORDED\` with
\`applicationId\` as \`resourceId\`. Generic — reusable by any app.

**Tool name prefix**
- \`LogicFunctionToolProvider.buildLogicFunctionToolName\` now produces
\`app_<name>\` instead of \`logic_function_<name>\`. Only affects tools
sourced from logic functions; other tool providers unchanged.

## Stats

- 16 files, +501 / −2
- 7 new files (1 command, 1 service × 2, 1 controller, 1 DTO, 2 modules)
- Typecheck: 7 pre-existing errors, zero new
- Prettier clean

## Behavior deltas

- **\`PRE_INSTALLED_APPS\` default = empty**: existing servers see no
change on merge.
- **\`ApplicationRegistrationVariable\` is now read by the executor**:
apps that were using manifest \`serverVariables\` but expecting them to
be ignored by the executor will now see them injected. No apps ship with
\`isTool: true\` logic functions today, so this is latent — first
consumer is Exa in PR 2.
- **Tool prefix**: currently no logic-function tools are named
\`logic_function_*\` in any production flow. The prefix change affects
only future tools emitted by \`LogicFunctionToolProvider\`.

## Risks

- **CDN unavailability at startup**: if the app registry CDN is down,
\`ensureRegistrationsExist\` logs warnings but doesn't block server
start. Installation on new workspaces during this window will find no
registrations and log a non-blocking error. Backfill command can retry
after CDN recovers.
- **Cold-start overhead**: \`ensureRegistrationsExist\` is called once
per process on bootstrap. Current configurable default is empty, so zero
overhead. When an admin sets \`PRE_INSTALLED_APPS\`, they accept one
HTTP call per package at boot.
- **Server-level variables flow**:
\`ApplicationRegistrationVariable.encryptedValue\` is shared by all
workspaces of a server. Appropriate for a single-tenant Exa key. Not
appropriate for per-tenant keys — those go in workspace-level
\`ApplicationVariable\` and override.

## Test plan

- [ ] \`npx nx typecheck twenty-server\` passes (verified: 7
pre-existing unrelated errors, zero new)
- [ ] Set \`PRE_INSTALLED_APPS=@twenty-apps/hello-world\` (or any real
npm-published app), \`HELLO_WORLD_API_KEY=xxx\`, restart server:
\`ApplicationRegistration\` row is upserted,
\`ApplicationRegistrationVariable\` for HELLO_WORLD_API_KEY is populated
(encrypted).
- [ ] Create a new workspace: the app is auto-installed,
\`ApplicationEntity\` row created, \`LogicFunctionEntity\` rows created.
- [ ] Existing workspace: run \`yarn nx run twenty-server:command
install-pre-installed-apps\`: apps install across all workspaces,
idempotent on re-run.
- [ ] Trigger a logic function that reads
\`process.env.HELLO_WORLD_API_KEY\`: value resolves from the
server-level \`ApplicationRegistrationVariable\`.
- [ ] Log a charge from the handler: \`POST /app/billing/charge\` with
\`Authorization: Bearer \$DEFAULT_APP_ACCESS_TOKEN\` body
\`{creditsUsedMicro: 1000, quantity: 1, unit: "INVOCATION",
operationType: "WEB_SEARCH"}\` → returns \`{success: true}\`,
\`USAGE_RECORDED\` event emitted with correct
\`resourceId=applicationId\`.
- [ ] Tool name generated by \`LogicFunctionToolProvider\` starts with
\`app_\`.

## What's NOT in this PR (PR 2 scope)

- The Exa app itself (\`packages/twenty-apps/...\` directory)
- Removing \`WebSearchTool\`, \`WebSearchService\`, \`ExaDriver\`,
\`web-search\` module
- Removing \`WEB_SEARCH_DRIVER\` config var
- Removing the current \`exa_web_search\` entry in
\`ActionToolProvider\`
- Chat preload list updated to \`app_exa_web_search\`
- Frontend \`getToolDisplayMessage\` branch for \`app_exa_web_search\`
- Setting \`PRE_INSTALLED_APPS\` default to include \`@twenty-apps/exa\`

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 21:59:00 +02:00
neo773andGitHub 20f7ba82d7 optimize workspace export command (#20000)
- Use `COPY` instead of `INSERT` for workspace tables, 40x faster
imports
- Multi value statements, 2x smaller file size
- Bump Batch size to 10K , remove COUNT query
- Add `formatPgCopyField` utility with unit tests
2026-04-23 09:42:49 +00:00
Paul RastoinandGitHub 61fdb613e6 Reset default app packages command (#19931)
# Introduction
If a self host creates its twenty instance using storage type local, and
then edit through the admin panel the storage type, the apps default
deps file won't be swapped to the new storage location

This command allow to manually rebuild them

## What could be done in addition
- We could display a modal in the admin panel when the user is editing
env variable that might have a side effect
- We might wanna rebuild the deps by default if we detect such a change
through the UI though we can't really if it's through the `.env` so I'm
not sure we wanna prio such logic
2026-04-21 13:32:59 +00:00
Charles BochetandGitHub 41ee6eac7a chore(server): bump current version to 2.0.0 and add 2.1.0 as next (#19907)
## Summary

We are releasing Twenty v2.0. This PR sets up the
upgrade-version-command machinery for the new release line:

- Move `1.23.0` into `TWENTY_PREVIOUS_VERSIONS` (it just shipped)
- Set `TWENTY_CURRENT_VERSION` to `2.0.0` (no specific upgrade commands
— this is just the major version cut)
- Set `TWENTY_NEXT_VERSIONS` to `['2.1.0']` so future PRs that
previously would have targeted `1.24.0` now target `2.1.0`
- Add empty `V2_0_UpgradeVersionCommandModule` and
`V2_1_UpgradeVersionCommandModule` and wire them into
`WorkspaceCommandProviderModule`
- Refresh the `InstanceCommandGenerationService` snapshots to reflect
the new current version (`2.0.0` / `2-0-` slug)

The `2-0/` directory is intentionally empty — there are no specific
upgrade commands for the v2.0 cut. New upgrade commands authored after
this merges should land in `2-1/` (or be generated against `--version
2.1.0`).

## Test plan

- [x] `npx jest` on the impacted upgrade test files
(`upgrade-sequence-reader`, `upgrade-command-registry`,
`instance-command-generation`) passes (41 tests, 8 snapshots)
- [x] `prettier --check` and `oxlint` clean on touched files
- [ ] Manual: open `nx run twenty-server:command -- upgrade --dry-run`
against a local stack with workspaces still on `1.23.0` and confirm the
sequence is computed without errors

Made with [Cursor](https://cursor.com)
2026-04-21 02:05:27 +02:00
neo773andGitHub ade55e293f fix 1.22 upgrade command add-workspace-id-to-indirect-entities (#19868)
/closes #19863
2026-04-20 13:19:10 +00:00
neo773andGitHub 1d575f0496 fix oauth permission check (#19829)
was regressed due to https://github.com/twentyhq/twenty/pull/19441
2026-04-18 11:20:23 +02:00
Charles BochetandGitHub 4fa2c400c0 fix(server): skip standard page layout widgets referencing missing field metadatas during 1.23 backfill (#19825)
## Summary

The 1.23 backfill command (`upgrade:1-23:backfill-record-page-layouts`)
creates standard page layout widgets from `STANDARD_PAGE_LAYOUTS`. Some
widgets reference field metadatas via `universalConfiguration` (e.g. the
`opportunity.owner` FIELD widget pointing at universal identifier
`20202020-be7e-4d1e-8e19-3d5c7c4b9f2a`).

If a workspace's matching field metadata does not exist or has a
different universal identifier (e.g. older workspaces created before
standard universal identifiers were backfilled), the runner throws

```
Field metadata not found for universal identifier: 20202020-be7e-4d1e-8e19-3d5c7c4b9f2a
```

and the entire migration for that workspace aborts. This was the
underlying cause behind the `Migration action 'create' for
'pageLayoutWidget' failed` error surfaced by #19823.
2026-04-17 23:59:13 +02:00
Charles BochetandGitHub fb5a1988b1 fix(server): log inner errors of WorkspaceMigrationRunnerException in workspace iterator (#19823)
## Summary

When a workspace migration action fails during workspace iteration (e.g.
during upgrade commands), only the wrapper message was logged:

```
[WorkspaceIteratorService] Error in workspace 7914ba64-...: Migration action 'create' for 'pageLayoutWidget' failed
```

The underlying error (transpilation/metadata/workspace schema) and its
stack were swallowed, making production debugging painful.

This PR adds a follow-up log entry for each inner error attached to a
`WorkspaceMigrationRunnerException`, including its message and stack
trace. The runner exception itself is untouched — it already exposes
structured `errors` (`actionTranspilation`, `metadata`,
`workspaceSchema`).

After this change, logs look like:

```
[WorkspaceIteratorService] Error in workspace 7914ba64-...: Migration action 'create' for 'pageLayoutWidget' failed
[WorkspaceIteratorService] Caused by actionTranspilation in workspace 7914ba64-...: <real reason>
    at ...
```

## Test plan

- [ ] Trigger a failing workspace migration (e.g. backfill record page
layouts) on a workspace and confirm the underlying cause + stack now
appear in logs.

Made with [Cursor](https://cursor.com)
2026-04-17 22:31:35 +02:00
Charles BochetandGitHub 3eeaebb0cc fix(server): make workspace:seed:dev --light actually seed only one workspace (#19822)
## Summary

The `--light` flag of `workspace:seed:dev` was supposed to seed a single
workspace for thin dev containers, but it was only filtering the rich
workspaces (Apple, YCombinator) — the `Empty3`/`Empty4` fixtures
introduced in #19559 for upgrade-sequence integration tests were always
seeded.

So `--light` actually produced **3** workspaces:
- Apple
- Empty3
- Empty4

In single-workspace mode (`IS_MULTIWORKSPACE_ENABLED=false`, the default
for the `twenty-app-dev` container),
[`WorkspaceDomainsService.getDefaultWorkspace`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service.ts)
returns the most recently created workspace — Empty4 — which has no
users. The prefilled `tim@apple.dev` therefore cannot sign in, which
breaks flows that depend on the default workspace such as `yarn twenty
remote add --local`'s OAuth handshake against the dev container.

This PR makes `--light` actually skip the empty fixtures so the dev
container ends up with a single workspace (Apple). The default (no flag)
invocation, used by `database:reset` for integration tests, still seeds
all four workspaces, so
`upgrade-sequence-runner-integration-test.util.ts` keeps working
unchanged.
2026-04-17 22:08:01 +02:00
WeikoandGitHub 3268a86f4b Skip backfill record page layouts for missing standard objects (#19799) 2026-04-17 13:09:11 +00:00
Charles BochetandGitHub 4ed6fcd19e chore: move TABLE_WIDGET view type migration to 1.23 fast instance command (#19797)
## Summary

- Relocates `AddTableWidgetViewTypeFastInstanceCommand` from `1-22/` to
`1-23/` and bumps its `@RegisteredInstanceCommand` version from `1.22.0`
to `1.23.0`. The original timestamp `1775752190522` is preserved so the
command slots chronologically into the existing 1.23 sequence;
auto-discovered via `@RegisteredInstanceCommand`, no module wiring
change needed.
- Same pattern as #19792 (move
`pageLayoutWidget.conditionalAvailabilityExpression` to 1.23).
2026-04-17 12:44:35 +00:00
Charles BochetandGitHub beeb8b7406 chore: move pageLayoutWidget.conditionalAvailabilityExpression migration to 1.23 fast instance command (#19792)
## Summary

- Replaces the standalone TypeORM migration
`1775654781000-addConditionalAvailabilityExpressionToPageLayoutWidget.ts`
with a registered fast instance command under
`packages/twenty-server/src/database/commands/upgrade-version-command/1-23/`,
so the `pageLayoutWidget.conditionalAvailabilityExpression` column is
created through the unified upgrade pipeline.
- Uses `ADD COLUMN IF NOT EXISTS` / `DROP COLUMN IF EXISTS` so the new
instance command is a safe no-op for environments that already applied
the previous TypeORM migration.
- Keeps the original timestamp `1775654781000` so the command slots
chronologically into the existing 1.23 sequence; auto-discovered via
`@RegisteredInstanceCommand`, no module wiring needed.

## Context

Reported error when creating a new workspace on `main`:

> column PageLayoutWidgetEntity.conditionalAvailabilityExpression does
not exist

Aligns this column addition with the rest of the 1.23 schema changes
that already use the instance-command pattern.
2026-04-17 11:40:05 +00:00
WeikoandGitHub 5cd8b7899d shouldIncludeRecordPageLayouts deprecation (#19774)
## Context
Deprecating shouldIncludeRecordPageLayouts in preparation for page
layout release.

See new workspace with standard page layout from standard app
<img width="570" height="682" alt="Screenshot 2026-04-16 at 18 35 23"
src="https://github.com/user-attachments/assets/bf7fa621-d40d-4c29-8d96-537c58b3eb40"
/>
2026-04-17 11:32:10 +00:00
Paul RastoinandGitHub bf410ae438 upgrade:status command (#19584)
## Introduction
Introducing a new command in order to determine the curent twenty
instance and workspaces status as it's not stored in database but a
derivation of each current curors


## `upgrade:status` all healthy
<img width="1376" height="1202" alt="image"
src="https://github.com/user-attachments/assets/e90d6987-07d2-4b6b-b573-105249aca325"
/>

## `upgrade:status` Nearly use cases
<img width="1442" height="1304" alt="image"
src="https://github.com/user-attachments/assets/c336cb9d-eb9d-4c7d-9392-ec1ef54a7326"
/>

## `upgrade:status --failed-only`
<img width="1442" height="940" alt="image"
src="https://github.com/user-attachments/assets/93a3dfdb-0d2f-4a01-b185-118e5cf0a078"
/>

## `upgrade:status -w aa8fdcb1-8ee1-4012-98af-44a97caa7411 -w
20202020-1c25-4d02-bf25-6aeccf7ea419 -w
20202020-1c25-4d02-bf25-6aeccf7ea412`
<img width="1486" height="928" alt="image"
src="https://github.com/user-attachments/assets/ec1b1abc-46e8-4e36-9799-ab3a4b85e410"
/>
2026-04-16 16:05:02 +00:00
Paul RastoinandGitHub 2413af0ba9 [run-instance-commands] Preserve fast slow sequentiality (#19757)
# Introduction
The command was wrongly running all fast and then all slow ignoring
instance commands segment
Leading to 
```ts
1.23.0_AddGlobalObjectContextToCommandMenuItemAvailabilityTypeFastInstanceCommand_1776090711153 executed successfully
[Nest] 32679  - 04/16/2026, 1:21:07 PM     LOG [InstanceCommandRunnerService] 1.23.0_DropWorkspaceVersionColumnFastInstanceCommand_1785000000000 executed successfully
[Nest] 32679  - 04/16/2026, 1:21:07 PM     LOG [InstanceCommandRunnerService] 1.22.0_BackfillWorkspaceIdOnIndirectEntitiesSlowInstanceCommand_1775758621018 executed successfully
[Nest] 32679  - 04/16/2026, 1:21:07 PM     LOG [RunInstanceCommandsCommand] Instance commands completed
```

No prod/self host impact as 1.23 hasn't been released yet
2026-04-16 12:14:47 +00:00
2b5b8a8b13 Link command menu items to specific page layout (#19706)
- Add a `pageLayoutId` foreign key to `CommandMenuItem`, allowing
command menu items to be scoped to a specific page layout instead of
being globally available
- Filter command menu items by the current page layout on the frontend.
Items with a `pageLayoutId` only appear when viewing that layout, while
items without one remain globally visible
- Create an effect to track the current page layout ID
- Include a seed example: a "Show Notification" command pinned to the
Star history standalone page layout

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-16 12:07:36 +00:00
Paul RastoinandGitHub 5583254f58 Remove workspace-migrations deadcode (#19752)
Was a previous twenty version upgrade command util
2026-04-16 08:52:58 +00:00
Paul RastoinandGitHub a4cc7fb9c5 [Upgrade] Fix workspace creation cursor (#19701)
## Summary

### Problem

The upgrade migration system required new workspaces to always start
from a workspace command, which was too rigid. When the system was
mid-upgrade within an instance command (IC) segment, workspace creation
would fail or produce inconsistent state.

### Solution

#### Workspace-scoped instance command rows

Instance commands now write upgrade migration rows for **all
active/suspended workspaces** alongside the global row. This means every
workspace has a complete migration history, including instance command
records.

- `InstanceCommandRunnerService` reloads `activeOrSuspendedWorkspaceIds`
immediately before writing records (both success and failure paths) to
mitigate race conditions with concurrent workspace creation.
- `recordUpgradeMigration` in `UpgradeMigrationService` accepts a
discriminated union over `status`, handles `error: unknown` formatting
internally, and writes global + workspace rows in batch.

#### Flexible initial cursor for new workspaces

`getInitialCursorForNewWorkspace` now accepts the last **attempted**
(not just completed) instance command with its status:

- If the IC is `completed` and the next step is a workspace segment →
cursor is set to the last WC of that segment (existing behavior).
- If the IC is `failed` or not the last of its segment → cursor is set
to that IC itself, preserving its status.

This allows workspaces to be created at any point during the upgrade
lifecycle, including mid-IC-segment and after IC failure.

#### Relaxed workspace segment validation

`validateWorkspaceCursorsAreInWorkspaceSegment` accepts workspaces whose
cursor is:
1. Within the current workspace segment, OR
2. At the immediately preceding instance command with `completed` status
(handles the `-w` single-workspace upgrade scenario).

Workspaces with cursors in a previous segment, ahead of the current
segment, or at a preceding IC with `failed` status are rejected.

### Test plan
created empty workspaces to allow testing upgrade with several active
workspaces
2026-04-15 15:41:10 +00:00
Charles BochetandGitHub a328c127f3 Fix standalone page migration failing on navigationMenuItem enum type change (#19724)
## Summary
- The `AddStandalonePageFastInstanceCommand` migration was failing with:
`default for column "type" cannot be cast automatically to type
core."navigationMenuItem_type_enum"`
- The migration was missing `DROP DEFAULT` / `SET DEFAULT` around the
`ALTER COLUMN TYPE` for `navigationMenuItem.type`. PostgreSQL cannot
automatically cast the existing default (`'VIEW'::old_enum`) to the new
enum type.
- The same 3-step pattern (DROP DEFAULT → ALTER TYPE → SET DEFAULT) was
already correctly applied for `pageLayout.type` in the same migration —
this fix brings `navigationMenuItem.type` in line.
2026-04-15 17:25:55 +02:00
WeikoandGitHub 5e83ad43de Update backfill page layout command (#19687) 2026-04-15 14:55:42 +00:00
EtienneandGitHub 94b8e34362 Object view widget - Introduce new TABLE_WIDGET view type (#19545)
closes
https://discord.com/channels/1130383047699738754/1491549365263667230/1491804729397743666
2026-04-15 13:30:37 +00:00
Raphaël BosiandGitHub a88d1f4442 Introduce standalone page (#19675)
Add support for standalone pages: a new `PageLayout` type
(`STANDALONE_PAGE`) that can be rendered independently at
`/page/:pageLayoutId`, not tied to any record or object context.

- New `STANDALONE_PAGE` page layout type
- New `PAGE_LAYOUT` navigation menu item type: adds a `pageLayoutId`
foreign key to `NavigationMenuItemEntity`, allowing sidebar items to
link directly to standalone pages
- New `GLOBAL_OBJECT_CONTEXT` command menu availability type: separates
object-context-dependent commands (Create Record, Import, Export, See
Deleted, Create View, Hide Deleted) from truly global ones, so
standalone pages only show relevant commands
- Frontend routing & rendering: adds a `/page/:pageLayoutId` route with
its own page component, header, and command menu
- Widget rendering refactor
- Instance commands: two fast 1.22 migrations: `pageLayoutId` column +
`STANDALONE_PAGE` enum, and `GLOBAL_OBJECT_CONTEXT` availability type
enum
- Workspace command: backfills existing command menu items from `GLOBAL`
to `GLOBAL_OBJECT_CONTEXT` where appropriate
- Dev seeds: adds a sample "Star History" standalone page with an iframe
widget for local development
2026-04-14 15:52:45 +00:00
Paul RastoinandGitHub ef328755bb Bump current version to 1.23.0 (#19683) 2026-04-14 12:04:44 +00:00
Paul RastoinandGitHub 3e699c4458 Fix upgrade commands discovery outside of cli (#19671)
# Introduction
We were allowing the sequence to be empty in the worker context that was
facing an edge case importing the UpgradeModule through the
WorkspaceModule god module, no commands were discovered and it was
throwing as the sequence must have at least one workspace commands to
allow a workspace creation

Though the issue was also applicable to the twenty-server `AppModule`
too that was not discovering any commands

## Integration tests were passing
The integration test were importing the `CommandModule` at the nest
testing app creating leading to asymmetric testing context
It was a requirement for a legacy commands import and global assignation

## Fix
The `UpgradeModule` now import both `WorkspaceCommandsProviderModule`
and `InstanceCommandProviderModule` which ships the commands directly in
the module
We could consider moving the commands into the `engine/upgrade` folder

## Concern
Bootstrap could become more and more long to load at both server and
worker start
When this becomes a problem we will have to only import the latest
workspace command or whatever
For the moment this is not worth it the risk to import not the latest
workspace command
2026-04-14 09:20:33 +00:00
Paul RastoinandGitHub 714f149b0c Move backfill page layout to 1.23 (#19670) 2026-04-14 08:39:24 +00:00
455022f652 Add ClickHouse-backed metered credit cap enforcement (#19586)
## Summary
Implements a ClickHouse-backed polling system to enforce metered-credit
caps for workflow executions, replacing reliance on Stripe billing
alerts. The system re-evaluates tier caps against live pricing on every
poll cycle, allowing price/tier changes to propagate immediately without
recreating Stripe alert objects.

## Key Changes

- **BillingUsageCapService**: New service that queries ClickHouse for
current-period credit usage and evaluates whether a subscription has
reached its metered-credit allowance (tier cap + credit balance)
  - `isClickHouseEnabled()`: Checks if ClickHouse is configured
- `getCurrentPeriodCreditsUsed()`: Sums creditsUsedMicro from usageEvent
table for a workspace within a billing period
- `evaluateCap()`: Determines if usage has reached the allowance by
reading live pricing from the subscription

- **EnforceUsageCapJob**: Cron job that polls all active subscriptions
and updates `hasReachedCurrentPeriodCap` on metered items
  - Runs every 2 minutes to keep cap enforcement in sync with live usage
- Supports shadow mode (log-only) via
`BILLING_USAGE_CAP_CLICKHOUSE_ENABLED` flag for safe rollout
- Continues processing after per-subscription errors with detailed
logging

- **EnforceUsageCapCronCommand**: CLI command to register the
enforcement cron job

- **MeteredCreditService**: Extracted
`extractMeteredPricingInfoFromSubscription()` as a pure function for
callers that already hold the subscription with pricing loaded, avoiding
redundant DB queries

- **Configuration**: Added `BILLING_USAGE_CAP_CLICKHOUSE_ENABLED` flag
to control enforcement mode (active vs. shadow)

- **Constants**: Added `METERED_OPERATION_TYPES` to define which
operation types count toward the metered product's credit cap

## Implementation Details

- The service queries ClickHouse for the sum of `creditsUsedMicro` in
the current billing period, matching Stripe meter semantics
- Pricing is re-read on every evaluation, so tier changes propagate
within one poll cycle without Stripe alert recreation
- The cron job only updates the database when the cap state actually
changes (no-op if already in the correct state)
- Shadow mode allows safe validation before enabling enforcement;
transitions are logged but not persisted
- Comprehensive test coverage for both the service and cron job,
including error handling and state transitions

https://claude.ai/code/session_01VksTSrYLXJVCPVBQhQdBTe

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-13 21:17:41 +02:00
Paul RastoinandGitHub 123d6241d7 Fix AddPermissionFlagRoleIdIndexFastInstanceCommand (#19654)
# Introduction
Index seemed to be missing in production only, as it's blocking the
whole migration release on other env that already implements the index
2026-04-13 18:14:31 +02:00
Paul RastoinandGitHub 87f5c0083f Prevent cross version upgrade mismatch in 1.22 (#19627)
## Introduction
As the new upgrade sequence engine is released in `1.22` it requires all
workspaces to be in `1.21.0` which mean they will have a cursor on the
sequence

As if if someone upgrades from `1.20` to `1.22` no `upgradeMigration`
will exist and throw a pretty basic `Could not find any cursor, database
might not been initialized correctly`

Here we allow a meaningful error
2026-04-13 14:53:12 +00:00
Paul RastoinandGitHub 5e7e5dd466 Colliding subject field fix on messageThread command (#19637) 2026-04-13 13:33:46 +00:00