## Summary
- Add `defineCommandMenuItem` and `definePageLayoutWidget` as standalone
SDK defines, mirroring the existing `definePageLayoutTab` pattern. Both
entities can still be declared nested inside their parent
(`defineFrontComponent.command` / `definePageLayout.tabs[].widgets[]`).
- Add `CommandMenuItem` and `PageLayoutWidget` to the `SyncableEntity`
enum and the dev-mode UI labels.
- Wire the SDK manifest-build to extract the two new defines into
top-level `commandMenuItems` / `pageLayoutWidgets` arrays on the
manifest, and the server aggregator to consume them through the existing
flat-entity converters.
- On the server, expose `Application.commandMenuItems` (relation + DTO +
service hydration in `findOneApplication`).
- On the front, list command menu items in the application content tab
and add a dedicated detail page with a settings tab, mirroring how
`frontComponents` are surfaced.
- Add `twenty add` templates and Vitest unit tests for both new defines.
- Document the standalone-vs-nested pattern in
`packages/twenty-sdk/README.md`.
### Why
Until now, command menu items could only be declared as the nested
`command:` field on `defineFrontComponent` — there was no way to
register a command menu item from a separate file or from another
package. The `SyncableEntity` enum had 12 values, while the server
already synced 18 (including `commandMenuItem` and `pageLayoutWidget`).
The same gap existed for `pageLayoutWidget`, which had no top-level
define despite being synced server-side. This PR closes both gaps and
aligns the SDK surface with what the server actually accepts.
The standalone defines coexist with the nested form — pick one per
entity, never both with the same `universalIdentifier` (the manifest
aggregator will throw on duplicates). The README now documents this.
## Test plan
- [x] `npx nx typecheck twenty-sdk` / `twenty-server` / `twenty-front`
- [x] `npx nx lint:diff-with-main twenty-front` / `twenty-server`
- [x] `npx nx lint twenty-sdk` / `twenty-shared`
- [x] New unit tests: `define-command-menu-item.spec.ts`,
`define-page-layout-widget.spec.ts`
- [x] Existing manifest extract config tests still pass
- [ ] Codegen `npx nx run twenty-front:graphql:generate
--configuration=metadata` should be re-run after merge — the generated
`graphql.ts` was patched manually to include `commandMenuItems` on
`Application` and the `FindOneApplication` document.
- [ ] Smoke test: scaffold an app with `twenty add` for both new entity
types, run `twenty dev`, confirm the dev UI shows them in the sync list
and the settings page surfaces command menu items in the content tab.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: martmull <martmull@hotmail.fr>
Co-authored-by: Charles Bochet <charles@twenty.com>
## Summary
Restructures the apps Getting Started doc around the three things a
developer actually has to do, so the mental model is visible upfront and
discoverable when something goes wrong.
**Why this matters:** the previous flow read as one continuous list of
bash commands and prompts, which made it easy to miss that scaffolding,
running a Twenty server, and live-syncing changes are three separate
concepts. When the user hits a failure (Docker not running, server not
up, auth not authorized), they have no mental map for which step they're
in — so they end up retrying `yarn twenty dev`, which is the only
command they remember.
## What changes
**[getting-started.mdx](https://github.com/twentyhq/twenty/blob/docs/restructure-getting-started-three-phases/packages/twenty-docs/developers/extend/apps/getting-started.mdx):**
- New summary table at the top showing the three-phase arc:
| Phase | What you do | Tool | Result |
|---|---|---|---|
| **1. Scaffold** | Generate the app's source code | `npx
create-twenty-app` | A TypeScript project on disk |
| **2. Run a server** | Start a Twenty server to sync into | Docker +
`yarn twenty server` | A running Twenty instance |
| **3. Sync** | Live-sync your code to the server | `yarn twenty dev` |
Your changes appear in the UI |
- Three top-level sections, one per phase, each ending with **"After
this phase: you have X"** so users can self-diagnose where they got
stuck.
- Phase 2 leads with the sentence that was missing before: *"Your app
needs a Twenty server to sync into. The server is a full Twenty instance
— UI, GraphQL API, PostgreSQL — running locally in Docker."* This is the
concept new users were missing.
- Removed the standalone *What are apps?* section — that's what the Core
Concepts page is for. Don't duplicate.
- Tightened wording throughout; same screenshots, same callouts, same
content depth.
**[core-concepts/apps.mdx](https://github.com/twentyhq/twenty/blob/docs/restructure-getting-started-three-phases/packages/twenty-docs/getting-started/core-concepts/apps.mdx):**
- Removed the install snippet (`npx create-twenty-app`, `cd`, `yarn
twenty dev`) — it duplicated Getting Started and the two examples used
different directory names.
- Updated the link card to reflect the new three-phase structure.
## Out of scope (mentioned for context, not done here)
- The "Docker is not running" message rewrite: separate PR
([#20280](https://github.com/twentyhq/twenty/pull/20280)).
- A `yarn twenty start` one-command bootstrap that auto-starts the
server before `dev`. Worth doing — keeping it out of this docs PR.
- Auto-offering to start the server when `yarn twenty dev` finds no
running one. Same.
- An "agent path" doc (single-page, imperative, for AI assistants) —
separate effort.
## Test plan
- [x] `npx nx lint twenty-docs` passes (no new warnings)
- [x] All `<Note>`, `<Warning>`, `<Card>`, image refs preserved
- [ ] Render and click through both pages once merged and previewed
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Summary
`catalog-sync` is a server-side admin action — it asks the connected
Twenty server to refresh its marketplace catalog from npm. It doesn't
operate on the local app code (like `build`, `deploy`, `publish`), so
having it sit at the same root level as those commands is a navigability
problem. With 13 commands at the root today, every needless one makes
the help output harder to scan.
This PR moves it under `server`:
```
# New (preferred)
yarn twenty server catalog-sync
yarn twenty server catalog-sync --remote production
# Old (still works, prints deprecation warning)
yarn twenty catalog-sync
```
Also slightly broadens the `server` group description from "Manage a
local Twenty server instance" to "Manage a Twenty server (local instance
and server-side actions)" since `catalog-sync` can target a remote.
## Help output (after)
```
$ yarn twenty --help
Commands:
...
catalog-sync [options] [Deprecated] Moved under server. Use `yarn twenty server catalog-sync`.
...
server Manage a Twenty server (local instance and server-side actions)
$ yarn twenty server --help
Commands:
start [options] Start a local Twenty server
stop [options] Stop the local Twenty server
logs [options] Stream Twenty server logs
status [options] Show Twenty server status
reset [options] Delete all data and start fresh
upgrade [options] [version] Upgrade the twenty-app-dev Docker image
catalog-sync [options] Trigger a marketplace catalog sync on the server
```
## Backwards compatibility
The top-level `yarn twenty catalog-sync` still works and runs the same
logic. It prints a yellow warning suggesting the new path, then executes
normally. Plan is to remove it in a future release.
## Test plan
- [x] `npx nx typecheck twenty-sdk` passes
- [x] `npx nx lint twenty-sdk` passes
- [x] `yarn twenty --help` shows the deprecated entry
- [x] `yarn twenty server --help` lists the new subcommand
- [x] `yarn twenty catalog-sync --help` shows the deprecation message in
the description
- [ ] End-to-end: invoking either path triggers a sync against a running
server
## Possible follow-ups
This is one slice of the bigger CLI flattening discussed offline. Other
natural moves: group `build/deploy/publish/install/uninstall/typecheck`
under an `app` group, group `add/exec/logs` under `entity`. Doing those
in their own PRs to keep blast radius small.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## 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>
# Introduction
Support multiple selected record ids for headless front components
### Changes
**Added:**
- `recordIds: string[]` field to `FrontComponentExecutionContext`
- `useRecordIds()` hook to get all selected record IDs
**Deprecated:**
- `recordId` field - use `recordIds` instead
- `useRecordId()` hook - use `useRecordIds()` instead
Backward compatibility is preserved
## Summary
The example directory name in our scaffolding instructions was
inconsistent across docs:
| Source | Name used |
|--------|-----------|
| `create-twenty-app` README | `my-twenty-app` |
| Getting Started (developer docs) | `my-twenty-app` |
| Core Concepts → Apps (intro doc) | `my-app` ⚠️ |
| `twenty-sdk` README | `my-app` ⚠️ |
This means a user reading the high-level Apps intro sees `my-app`, then
the official Getting Started guide and the scaffold use `my-twenty-app`.
Small but eroding for confidence on the very first command.
This PR aligns the two outliers to `my-twenty-app`. The `twenty-my-app`
example in `publishing.mdx` is left alone — that's an npm package name
example, not a directory name (different concept).
## Test plan
- [x] `grep -rn "my-app\b"` over source docs returns no other
directory-name occurrences
- [ ] Verify rendered docs after merge
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## 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>
##
The command pulls the image, compares it against the one the container
was created from, and only recreates the container if the image actually
changed. Your data volumes are preserved — only the container is
replaced.
## Summary
Follow-up to #20061, which added `rawBody?: string` to
`LogicFunctionEvent` / `RoutePayload` so logic functions can verify
HMAC-style webhook signatures (GitHub's `X-Hub-Signature-256`, Stripe,
…) against the exact bytes that were received.
That PR did not update the public SDK docs, so the new field is
effectively invisible to developers consuming `RoutePayload` from
`twenty-sdk`. This PR adds a single row to the `RoutePayload` structure
table in `logic-functions.mdx` so the field is discoverable.
Addresses the review feedback on
https://github.com/twentyhq/twenty/pull/20061#discussion_r3147065014.
Add missing info about how to edit navigation bar, add many-to-many
relation and get Enterprise key for Org license on self-hosted
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
## Summary
Introduces `definePageLayoutTab` so apps can attach a single tab (with
optional widgets) to an **existing** `pageLayout` referenced by
`pageLayoutUniversalIdentifier`. The parent layout can be standard, from
the same app, or from another app — mirroring how `defineField`
references an object via `objectUniversalIdentifier`.
This complements `definePageLayout`: use `definePageLayout` when you own
the entire layout, use `definePageLayoutTab` when you only want to add
to one.
```ts
import { definePageLayoutTab, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
export default definePageLayoutTab({
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001',
pageLayoutUniversalIdentifier: 'STANDARD-OR-OTHER-APP-PAGE-LAYOUT-UUID',
title: 'Hello World',
position: 1000,
icon: 'IconWorld',
layoutMode: PageLayoutTabLayoutMode.CANVAS,
widgets: [/* ... */],
});
```
## Changes
- **twenty-shared**: new top-level `pageLayoutTabs:
PageLayoutTabManifest[]` on `Manifest`, optional
`pageLayoutUniversalIdentifier` on `PageLayoutTabManifest`, new
`SyncableEntity.PageLayoutTab`.
- **twenty-sdk**:
- new `definePageLayoutTab` + `PageLayoutTabConfig` exports;
- manifest extraction wiring (`TargetFunction.DefinePageLayoutTab`,
`ManifestEntityKey.PageLayoutTabs`);
- dev-mode label/state for the new entity;
- CLI scaffold (`getPageLayoutTabBaseFile`) + unit tests for `npx
twenty-cli add`.
- **twenty-server**: convert top-level `pageLayoutTabs` (and their
widgets) into universal flat entities in
`computeApplicationManifestAllUniversalFlatEntityMaps`. Cross-app FK
validation on `pageLayoutUniversalIdentifier` is already handled by the
existing `FlatPageLayoutTab` validator.
- **docs**: new `definePageLayoutTab` accordion in `apps/layout.mdx`
with usage example and guidance vs `definePageLayout`.
- **CI / rich-app fixture**: `extra-tab.page-layout-tab.ts` exercises
the new flow with a front-component widget; `expected-manifest.ts` and
`manifest.tests.ts` updated.
# [Docs]: Fix blank Try Twenty button text in dark mode
## 🐛 Problem
The "Try Twenty" CTA button in the docs navbar appears blank/invisible
when users enable dark mode, making it impossible to click through to
the sign-up page.
**Issue:** #19965
## Root Cause
CSS specificity conflict in `packages/twenty-docs/custom.css`:
- The dark mode CSS rule targets only the `<a>` element
- Mintlify renders button text in a nested `<span>` with class
`text-white` (white color)
- The `text-white` Tailwind utility directly applies `color: rgb(255 255
255)` to the span
- This overrides the inherited dark color from the parent `<a>` element
- **Result:** White text on white background = invisible button
## Solution
Update the CSS selector in `packages/twenty-docs/custom.css` to target
both the `<a>` element AND the nested `<span>`:
**Before:**
```css
:is(.dark, [data-theme="dark"]) #topbar-cta-button a {
background-color: #ffffff !important;
color: #141414 !important;
}