Compare commits

..
Author SHA1 Message Date
Thomas TrompetteandGitHub c94aa7316a send X-Schema-Version header when metadataVersion is 0 (#18924)
Fixes https://github.com/twentyhq/twenty/issues/13103

Newly created workspaces start with metadataVersion = 0. The truthy
check this.currentWorkspace?.metadataVersion && {…} treated 0 as falsy,
so the X-Schema-Version header was never sent. Without this header, the
backend's schema mismatch detection is bypassed and raw validation
errors leak to Sentry. Replaced with isDefined() check.

Also replaced bare negation checks with isDefined() in the backend
validation hook for consistency.
2026-03-24 18:13:31 +01:00
Raphaël BosiandGitHub cac92bffe3 Refactor backfill-command-menu-items to use a single validateBuildAndRun call (#18923)
Refactored `backfill-command-menu-items` upgrade command to remove the
manual `QueryRunner` transaction and wrap standard and trigger workflow
version command menu item creation into a single
`validateBuildAndRunWorkspaceMigration` call.
2026-03-24 18:13:10 +01:00
Charles BochetandGitHub d9eef5f351 Fix visual regression dispatch for fork PRs (#18921)
## Summary
- Visual regression dispatch was failing for external contributor PRs
because fork PRs don't have access to repo secrets
(`CI_PRIVILEGED_DISPATCH_TOKEN`)
- Moved the dispatch from inline jobs in `ci-front.yaml` / `ci-ui.yaml`
to a new `workflow_run`-triggered workflow
- `workflow_run` runs in the base repo context and always has access to
secrets, regardless of whether the PR is from a fork
- Follows the same pattern already used by `post-ci-comments.yaml` for
breaking changes dispatch
- Handles the fork case where `workflow_run.pull_requests` is empty by
falling back to a head label search

## Test plan
- [ ] Verify CI Front and CI UI workflows still pass without the removed
jobs
- [ ] Verify the new `visual-regression-dispatch.yaml` triggers after CI
Front / CI UI complete
- [ ] Test with a fork PR to confirm the dispatch succeeds
2026-03-24 18:13:00 +01:00
77bade8114 i18n - docs translations (#18925)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-24 17:42:35 +01:00
8696e3b7ec i18n - translations (#18922)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-24 17:21:18 +01:00
856c72c5d6 i18n - translations (#18920)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-24 17:15:17 +01:00
cec23e89fa Fix batch update optimistic and prevent accidental mass-update (#17213)
## Problem
- ⚠️ Multi-edit could silently update ALL records of an object with no
undo, no ctrl-z
- After a batch update the table showed stale data —
`useIncrementalUpdateManyRecords` had no explicit `findMany` refetch and
`skipOptimisticEffect` was missing
- Clicking inside the side panel deselected all kanban cards —
`RecordBoardClickOutsideEffect` uses `refs:[]` and the side panel had no
`data-click-outside-id`
- Clicking a currency/select dropdown inside the panel also triggered
deselection — `FloatingPortal` renders outside the side panel DOM,
bypassing the click-outside-id exclusion
- An empty selection silently matched every record —
`computeContextStoreFilters` returned `undefined` filter when
`selectedRecordIds` was `[]`

## Fix
- Table refreshes correctly after batch update —
`useRefetchFindManyRecords` explicitly refetches `FindMany<Object>`
queries; `useIncrementalUpdateManyRecords` adds `skipOptimisticEffect:
true` and calls it in `finally`
- Clicking the side panel no longer deselects kanban cards —
`SidePanelForDesktop` carries `data-click-outside-id`;
`RecordBoardClickOutsideEffect` +
`RecordTableBodyFocusClickOutsideEffect` exclude it
- Clicking dropdowns inside the panel no longer deselects either —
`ParentClickOutsideIdContext` propagates the side panel ID into
`FloatingPortal` content via `DropdownInternalContainer`
- Empty selection can no longer match all records —
`computeContextStoreFilters` returns `{ id: { in: [] } }` instead of
`undefined`
- Apply is disabled with no selection; a confirmation modal shows the
count + no-undo warning before executing —
`UpdateMultipleRecordsContainer`

## Not included
- Undo / snapshot restore — requires backend changes, out of scope

## Blast radius
- `ParentClickOutsideIdContext` touches `DropdownInternalContainer` (207
`<Dropdown>` usages). `parentClickOutsideId` is `undefined` everywhere
outside the side panel → attribute not rendered → zero behavioral change
for existing consumers.

---------

Co-authored-by: Samuel Arbibe <samuelarbibe@Samuels-MacBook-Pro.local>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-03-24 17:14:30 +01:00
Raphaël BosiandGitHub 49afe5dbc4 Refactor: Unify command menu item execution (#18908)
- Makes engineComponentKey non-nullable on CommandMenuItem. Adds two new
engine component keys: TRIGGER_WORKFLOW_VERSION and
FRONT_COMPONENT_RENDERER to cover the former standalone cases.
- Unifies the previously separated engine, front component and workflow
runners into a single `CommandRunner` component with a unified
`useMountCommand` hook that handles all command types.
2026-03-24 15:59:52 +00:00
27c0ca975f Fix navigation “add before/after” insertion index (#18879)
Before: 
<video
src="https://github.com/user-attachments/assets/f0b0740d-414f-4c59-a712-cdf96d4f3eb1"
/>


After:
<video
src="https://github.com/user-attachments/assets/48ce878c-4a29-4666-89d4-4d60882dd5ab"
/>

---------

Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-03-24 15:49:07 +00:00
341c13bf32 Fix documentation (#18917)
as title

Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-03-24 15:00:37 +00:00
37787defc2 Fix readme (#18918)
as title

Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-03-24 15:00:10 +00:00
Charles BochetandGitHub 611947e031 fix: metadata store lifecycle during sign-in, sign-out, and locale change (#18901)
## Summary

Fixes metadata lifecycle bugs during sign-in, sign-out, and locale
change:

- **Cross-tab sign-out**: Broadcasts sign-out via `BroadcastChannel` so
other tabs clear their session gracefully instead of hitting stale-token
errors
- **SSE teardown on sign-out**: Handles `UNAUTHENTICATED`/`FORBIDDEN`
errors in the SSE event stream effect instead of throwing unhandled
errors
- **Locale switch resilience**: `invalidateAndReload` now invalidates
collection hashes instead of clearing the store to empty, so components
never see 0 metadata items during the reload transition
- **Sign-in background mock**: Uses non-throwing
`objectMetadataItemFamilySelector` instead of hooks that throw on
missing metadata
- **View name placeholders**: Guards against `undefined` `viewName`
during metadata transitions (the minimal metadata query doesn't include
`name`)
- **Session cleanup**: Selective `localStorage` clearing
(`clearSessionLocalStorageKeys`) preserves metadata keys;
`clearAllSessionLocalStorageKeys` for full clears
- **Metadata reload API**: New `useMetadataStoreActions` hook as the
high-level API for metadata lifecycle operations (`applyMockedMetadata`,
`invalidateAndReload`, `loadMockedMetadataAtomic`)

## Test plan

- [ ] Sign out on Tab A → Tab A shows sign-in page with no console
errors
- [ ] Tab B (logged in) receives cross-tab broadcast and redirects to
sign-in
- [ ] No "Forbidden resource" SSE errors in console during sign-out
- [ ] Change language in Settings > Experience → no crash, metadata
refreshes in background
- [ ] Sign back in after sign-out → metadata loads correctly, app is
functional
- [ ] Re-sign-in after locale change → correct locale is preserved
2026-03-24 14:57:53 +00:00
martmullandGitHub 2317a701bd Fix double arrow (#18916)
## Before

<img width="1004" height="612" alt="image"
src="https://github.com/user-attachments/assets/f0f832eb-d909-449f-88c1-1d30d2ec2c53"
/>


## after

<img width="971" height="555" alt="image"
src="https://github.com/user-attachments/assets/dc751866-85e8-4a3c-8100-2cf109969970"
/>
2026-03-24 14:33:37 +00:00
4c6e102493 Display found items after full items loaded (#18914)
The performCombinedFindManyRecords call was previously fire-and-forget
(.then()), meaning the loading state was set to false and the picker
became interactive before the full records were written into the Apollo
cache.

Fixes https://github.com/twentyhq/twenty/issues/17669

Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-03-24 14:05:22 +00:00
c1ec5567ce fix: insert-before folder for workspace DnD and add-to-nav drags (#18789)
Closes [#2294](https://github.com/twentyhq/core-team-issues/issues/2294)


https://github.com/user-attachments/assets/f7e0da72-dd08-46c4-92d7-be0453aaadd9

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-03-24 14:02:01 +00:00
Baptiste DevessierandGitHub 7a1780e415 Fix duplicate views creation in command (#18900)
Ensure the command doesn't fail on workspaces who got standard views
seeded the 13/03
2026-03-24 13:56:55 +00:00
4104b1d2bc i18n - translations (#18915)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-24 14:59:22 +01:00
Charles BochetandGitHub 7b6fb52df7 fix: validate blocknote JSON in rich text fields (#18902)
## Summary
- **Backend**: Add JSON validation for the `blocknote` subfield in rich
text API inputs — rejects values that aren't valid JSON or aren't arrays
(BlockNote content is always `PartialBlock[]`). This prevents corrupted
data from being persisted to the database.
- **Frontend**: Replace all 5 unprotected `JSON.parse` calls on
blocknote content with the safe `parseJson` utility from
`twenty-shared`. Invalid content now degrades gracefully (empty block /
empty string / unchanged passthrough) instead of crashing the app.
- **Tests**: Added integration tests for invalid blocknote JSON (both
GraphQL and REST), unit tests for the new validation, and updated
existing test constants to use valid BlockNote JSON.

## Context
A user reported a `SyntaxError: Expected ',' or ']' after array element`
crash caused by malformed blocknote JSON stored in the database. The
data had `"children":[]` nested inside the `content` array instead of as
a sibling property. The API accepted this invalid JSON because it only
validated that `blocknote` was a string, not that it contained valid
JSON. On the frontend, 5 call sites used bare `JSON.parse` with no error
handling, causing a white-screen crash.

## Test plan
- [x] Unit tests pass: `validate-rich-text-field-or-throw.util.spec.ts`
(10/10)
- [x] Integration tests pass: `rich-text-field-create-input-validation`
(8/8)
- [ ] Verify creating a note with valid rich text still works end-to-end
- [ ] Verify API returns clear error when blocknote contains invalid
JSON
- [ ] Verify frontend renders empty block instead of crashing when
encountering corrupted data

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-03-24 14:53:15 +01:00
EtienneandGitHub 56ea79d98c Perf investigation - Time qgl query (#18911)
Added a new IS_GRAPHQL_QUERY_TIMING_ENABLED feature flag that, when
activated per workspace, logs the execution time and operation name of
every GraphQL query across both the standard Yoga pipeline and the
direct execution fast path. The timing context propagates via
AsyncLocalStorage to also instrument buildColumnsToSelect and
formatResult — giving a breakdown of column selection and result
transformation costs without any caller changes.
2026-03-24 13:41:15 +00:00
martmullandGitHub d3c7b0131d Fix lambda driver (#18907)
as title
2026-03-24 13:33:39 +00:00
Abdul RahmanGitHubcubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
ec459d8dc8 Fix cropped view/link overlay icons in collapsed navigation sidebar (#18883)
<img width="45" height="670" alt="Screenshot 2026-03-24 at 8 46 31 AM"
src="https://github.com/user-attachments/assets/6eba1854-c279-46d8-a0d0-3034a41a3ce8"
/>

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-03-24 13:05:59 +00:00
neo773andGitHub e0b36c3931 fix: ai providers json formatting (#18910)
fixes CI
2026-03-24 12:53:34 +01:00
Baptiste DevessierandGitHub 8eff9efab2 Fix rich text widget edition (#18899)
Fixes https://github.com/twentyhq/twenty/issues/18894
2026-03-24 11:09:41 +00:00
960c80999e i18n - docs translations (#18903)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-24 11:41:29 +01:00
martmullandGitHub cc2be505c0 Fix twenty app dev image (#18852)
as title
2026-03-24 09:31:05 +00:00
493830204a [AI] Fix new chat button stacking side panel and auto-focus editor (#18875)
closes
https://discord.com/channels/1130383047699738754/1480984504737861853
https://discord.com/channels/1130383047699738754/1481210013950279740

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-03-24 09:26:33 +01:00
Félix MalfaitandGitHub dd84ab25df chore: optimize app-dev Docker image and add CI test (#18856)
## Summary

- **Reduce app-dev image size** by stripping ~60MB of build artifacts
not needed at runtime from the server build stage: `.js.map` source maps
(29MB), `.d.ts` type declarations (9MB), compiled test files (14MB), and
unused package source directories (~9MB).
- **Add CI smoke test** for the `twenty-app-dev` all-in-one Docker
image, running in parallel with the existing docker-compose test. Builds
the image, starts the container, and verifies `/healthz` returns 200.

## Test plan

- [x] Built image locally and verified server, worker, Postgres, and
Redis all start correctly
- [x] Verified `/healthz` returns 200 and frontend serves at `/`
- [ ] CI `test-compose` job passes (existing test, renamed from `test`)
- [ ] CI `test-app-dev` job passes (new parallel job)

Made with [Cursor](https://cursor.com)
2026-03-24 08:44:30 +01:00
fd044ba9a2 chore: sync AI model catalog from models.dev (#18885)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

This PR updates pricing, context windows, and model availability based
on the latest data.
New models meeting inclusion criteria (tool calling, pricing data,
context limits) are added automatically.
Deprecated models are detected based on cost-efficiency within the same
model family.

**Please review before merging** — verify no critical models were
incorrectly deprecated.

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-03-24 07:19:06 +01:00
Charles BochetandGitHub 69fe6fcadd chore: add explicit return type to getBackgroundColor (#18878)
Trivial typing improvement to trigger CI Front + CI UI visual regression
pipelines.
2026-03-24 01:10:22 +01:00
Charles BochetandGitHub 0ded15b363 Add visual regression CI for twenty-ui (#18877)
## Summary

- New workflow `ci-visual-regression.yaml` that runs on PRs touching
`twenty-ui` or `twenty-shared`
- Builds `twenty-ui` storybook and uploads the tarball as a GitHub
Actions artifact
- Dispatches to `twentyhq/ci-privileged` which handles the pixel-diff
comparison and posts a PR comment

### Flow

```
twenty CI (this PR)          ci-privileged                  pixel-perfect
─────────────────          ──────────────                  ──────────────
Build storybook
Upload artifact
Dispatch ──────────────►  Download artifact
                          Upload to S3 (OIDC)
                          POST /import-from-storage ────►  Import build
                          POST /diffs/run ──────────────►  Screenshots + diff
                          ◄──────────────────────────────  Diff report JSON
                          Post PR comment
```

Companion PRs:
- https://github.com/twentyhq/ci-privileged/pull/1 (ci-privileged
workflow)
- https://github.com/twentyhq/twenty-infra/pull/497 (OIDC trust + Helm
cleanup)
- https://github.com/twentyhq/pixel-perfect/pull/5 (API simplification)

## Test plan

- [ ] Merge companion PRs first and configure secrets/environments
- [ ] Open a test PR touching twenty-ui, verify storybook builds and
dispatch fires
- [ ] Verify visual regression comment appears on the PR
2026-03-24 00:15:45 +01:00
Charles BochetandGitHub 708e53d829 Fix multi-select option removal crashing when records contain removed values (#18871)
## Summary

- Fixes a bug where removing an option from a MULTI_SELECT field fails
with `invalid input value for enum` when existing records contain the
removed value alongside surviving values.
- The root cause was the `ELSE` branch in `updateArrayEnum` which tried
to cast removed enum values (e.g. `DISTRIBUTOR`) to the new enum type
that no longer includes them.
- The fix replaces the `ELSE` cast with a NULL-producing implicit CASE
default and uses `array_agg(...) FILTER (WHERE mapped_value IS NOT
NULL)` to silently strip removed values from existing arrays.

### Before (bug)
```sql
-- ELSE branch tries to cast removed value to new enum → crash
CASE unnest_value::text
  WHEN 'IMPL' THEN 'IMPL'::new_enum
  WHEN 'APP' THEN 'APP'::new_enum
  ELSE unnest_value::text::new_enum  -- 'DISTRIBUTOR' fails here
END
```

### After (fix)
```sql
-- No ELSE: removed values produce NULL, filtered out by array_agg
SELECT array_agg(mapped_value) FILTER (WHERE mapped_value IS NOT NULL)
FROM (
  SELECT CASE unnest_value::text
    WHEN 'IMPL' THEN 'IMPL'::new_enum
    WHEN 'APP' THEN 'APP'::new_enum
  END AS mapped_value
  FROM unnest(old_column) AS unnest_value
) enum_mapping
```
2026-03-23 20:06:34 +00:00
e2c85b5af0 Fix workspace dropdown truncation and center auth titles (#18869)
# Before

<img width="543" height="315" alt="CleanShot 2026-03-23 at 18 37 17"
src="https://github.com/user-attachments/assets/3a8233f8-507d-40e3-b4f0-0aae325bc4a8"
/>


# After

<img width="230" height="167" alt="CleanShot 2026-03-23 at 18 38 06"
src="https://github.com/user-attachments/assets/dda637d8-766a-4dc4-9909-78edef866c88"
/>

+ video: (long & short title)


https://github.com/user-attachments/assets/5373d168-6ffc-495b-909c-27fc1e68e712


also fixed text not centered in onboarding

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-23 21:06:14 +01:00
Thomas TrompetteandGitHub c6d4162b73 Fix: Background color selected row + border bottom settings tab (#18870)
<img width="1285" height="73" alt="Capture d’écran 2026-03-23 à 18 58
09"
src="https://github.com/user-attachments/assets/a01cfc8e-6492-4ebe-a47a-b504a73e616c"
/>
<img width="574" height="59" alt="Capture d’écran 2026-03-23 à 18 58
39"
src="https://github.com/user-attachments/assets/4947ec02-e151-48fb-87e9-86dbc0ed4fe9"
/>
2026-03-23 19:22:28 +00:00
93de331428 i18n - translations (#18873)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-23 19:36:47 +01:00
Abdul RahmanandGitHub 24055527c8 Rename "New sidebar item" to "New menu item" (#18859) 2026-03-23 18:21:41 +00:00
EtienneandGitHub c58bf9cf06 Fix e2e test (#18866)
Three feature flags changed the UI:
IS_NAVIGATION_MENU_ITEM_ENABLED — Sidebar no longer has a default
"People" link → navigate via URL instead
IS_COMMAND_MENU_ITEM_ENABLED — Button label is now "Create new Person"
(interpolated from object name) instead of static "Create new record"
IS_JUNCTION_RELATIONS_ENABLED — Company is now a junction relation
("Previous Companies") displayed inline, no longer a boxed
dynamic-relation-widget on the record page
2026-03-23 16:56:15 +00:00
Baptiste DevessierandGitHub 07a4cf2d26 Ensure command backfills all relations as Field widget (#18858)
\+ add missing Field widget for workflow relations

Copies the logic of the injectRelationWidgetsIntoLayout front-end
function
2026-03-23 17:49:05 +01:00
Raphaël BosiandGitHub 1cfdd2e8ed Update BackfillCommandMenuItemsCommand with workflow backfill (#18848)
Updated upgrade command to also backfill command menu items for
workflows.
This has been done in the same command because we need to enable the
feature flag once both operations are complete: the creation of the
standard command menu items and the creation of workflow command menu
items.
2026-03-23 17:48:50 +01:00
Félix MalfaitGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>EtienneEtienne
6f4f7a1198 fix: add security headers to file serving endpoints to prevent stored XSS (#18857)
## Summary

- File serving endpoints (`GET file/:fileFolder/:id` and `GET
public-assets/...`) were piping S3/local file streams directly to the
response without any HTTP headers, allowing a stored XSS attack via
uploaded HTML files rendered inline on the CRM origin.
- Adds `Content-Type`, `Content-Disposition`, and
`X-Content-Type-Options: nosniff` headers to all file serving responses.
Only known-safe MIME types (images, PDF, plain text, audio, video) are
served inline; everything else (HTML, SVG, XML, etc.) forces
`Content-Disposition: attachment` to trigger download instead of
rendering.
- New `setFileResponseHeaders` utility with an explicit allowlist of
inline-safe MIME types.

## Test plan

- [x] Unit tests pass (9 tests including 2 new ones: header assertions
and attachment-disposition for HTML)
- [x] Lint clean (`lint:diff-with-main`)
- [x] Typecheck clean (`nx typecheck twenty-server`)
- [ ] Manual: upload an HTML file via `uploadWorkflowFile`, access the
returned URL — should download instead of rendering
- [ ] Manual: upload a PNG image, access the URL — should render inline
with correct `Content-Type: image/png`
- [ ] Manual: verify `X-Content-Type-Options: nosniff` header is present
on all file responses


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Etienne <etiennejouan@users.noreply.github.com>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-03-23 16:19:16 +01:00
Charles BochetandGitHub 0626f3e469 Fix deploy: remove stale @ai-sdk/groq from yarn.lock (#18863)
## Summary

- The front deploy to S3 is failing because `@ai-sdk/groq` was removed
from `packages/twenty-server/package.json` but the `yarn.lock` was never
updated
2026-03-23 15:39:35 +01:00
Félix MalfaitandGitHub 630f3a0fd7 chore: trigger automerge for AI catalog sync PRs (#18855)
## Summary
- Adds a `repository-dispatch` step to the AI catalog sync workflow
(`ci-ai-catalog-sync.yaml`) that notifies `twenty-infra` when a sync PR
is created
- This allows the automerge workflow in `twenty-infra` to reactively
merge the PR instead of waiting for the next cron run

## Test plan
- [ ] Verify `TWENTY_INFRA_TOKEN` secret is available (same one used by
i18n workflows)
- [ ] Trigger the AI catalog sync workflow manually and confirm the
dispatch fires

Made with [Cursor](https://cursor.com)
2026-03-23 13:33:19 +01:00
b813d64324 chore: sync AI model catalog from models.dev (#18854)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

This PR updates pricing, context windows, and model availability based
on the latest data.
New models meeting inclusion criteria (tool calling, pricing data,
context limits) are added automatically.
Deprecated models are detected based on cost-efficiency within the same
model family.

**Please review before merging** — verify no critical models were
incorrectly deprecated.

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-03-23 13:14:09 +01:00
Félix MalfaitandGitHub c9ca4dfa14 fix: run AI catalog sync as standalone script to avoid DB dependency (#18853)
## Summary
- The `ci-ai-catalog-sync` cron workflow was failing because the
`ai:sync-models-dev` NestJS command bootstraps the full app, which tries
to connect to PostgreSQL — unavailable in CI.
- Converted the sync logic to a standalone `ts-node` script
(`scripts/ai-sync-models-dev.ts`) that runs without NestJS, eliminating
the database dependency.
- Removed the `Build twenty-server` step from the workflow since it's no
longer needed, making the job faster.

## Test plan
- [x] Verified the standalone script runs successfully locally via `npx
nx run twenty-server:ts-node-no-deps-transpile-only --
./scripts/ai-sync-models-dev.ts`
- [x] Verified `--dry-run` flag works correctly
- [x] Verified the output `ai-providers.json` is correctly written with
valid JSON (135 models across 5 providers)
- [x] Verified the script passes linting with zero errors
- [ ] CI should pass without requiring a database service

Fixes:
https://github.com/twentyhq/twenty/actions/runs/23424202182/job/68135439740

Made with [Cursor](https://cursor.com)
2026-03-23 13:07:53 +01:00
Félix MalfaitandGitHub 2dfa742543 chore: improve i18n workflow to prevent stale compiled translations (#18850)
## Summary

- **Add `lingui:compile` to Dockerfile** before both the server and
frontend build stages, ensuring compiled translation catalogs are always
fresh regardless of git state
- **Add `repository-dispatch` to i18n workflows** (`i18n-push.yaml` and
`i18n-pull.yaml`) to trigger reactive automerge in `twenty-infra` when
the i18n PR is ready, replacing the 15-minute polling approach

## Context

Users sometimes see "Uncompiled message detected" errors because
releases can be cut from `main` before the i18n PR (with freshly
compiled translation catalogs) has been merged. This creates a race
condition between new translatable strings landing on `main` and their
compiled catalogs being available.

These changes fix this in two ways:
1. **Safety net in builds**: Every Docker build now compiles
translations before building, so even if compiled catalogs in git are
stale, the build artifact is always correct
2. **Faster i18n PR merges**: Instead of a 15-minute cron polling for
i18n PRs, the workflows now notify `twenty-infra` immediately when
translations are ready, reducing merge latency from ~15 minutes to ~1
minute

Companion PR in twenty-infra: twentyhq/twenty-infra
(feat/i18n-reactive-automerge)

## Test plan

- [ ] Verify `TWENTY_INFRA_TOKEN` secret is available to i18n workflows
- [ ] Docker build still succeeds with the added `lingui:compile` steps
- [ ] i18n-push triggers automerge in twenty-infra after pushing changes
- [ ] i18n-pull triggers automerge in twenty-infra after pulling
translations


Made with [Cursor](https://cursor.com)
2026-03-23 12:53:31 +01:00
e618cc6cf9 i18n - translations (#18846)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-23 10:36:08 +01:00
Félix MalfaitandGitHub bb9e3c44a1 Show AI provider sections regardless of billing status (#18845)
## Summary

- Removes the `isBillingEnabled` guard that was hiding the Providers and
Custom Providers sections in the Admin AI settings
- The `GET_AI_PROVIDERS` query was being skipped when billing was
enabled, and the provider UI sections were conditionally hidden —
there's no reason to gate provider configuration behind billing status
- Cleans up the now-unused `billingState` and `useAtomStateValue`
imports

## Test plan

- [ ] Verify Providers and Custom Providers sections are visible in
Admin > AI settings on cloud (billing enabled)
- [ ] Verify they remain visible on self-hosted (billing disabled)


Made with [Cursor](https://cursor.com)
2026-03-23 10:29:32 +01:00
77d4bd9158 Add billing usage analytics dashboard with ClickHouse integration (#18592)
## Summary
This PR adds a comprehensive billing usage analytics feature that
provides detailed breakdowns of credit consumption across execution
types, users, resources, and time periods. The implementation includes a
new ClickHouse-backed analytics service, GraphQL API endpoint, and a
frontend dashboard component.

## Key Changes

### Backend
- **New BillingAnalyticsService**: Queries ClickHouse for usage
breakdowns by user, resource, execution type, and time series data
- **BillingEventWriterService**: Writes billing events to ClickHouse for
analytics while maintaining best-effort semantics (never blocks Stripe
billing)
- **ClickHouse Schema**: Added `billingEvent` table with 3-year TTL for
storing detailed billing event data
- **GraphQL Resolver**: New `getBillingAnalytics` query that aggregates
usage data for the current billing period, protected by feature flag and
billing permissions
- **Enhanced BillingUsageEvent**: Added `userWorkspaceId` field to track
per-user credit consumption
- **AI Billing Integration**: Updated AI billing service to pass
`userWorkspaceId` when recording usage events

### Frontend
- **SettingsBillingAnalyticsSection**: New component displaying:
  - Usage breakdown by execution type with progress bars
  - Daily usage time series chart (28-day view)
  - Per-user credit consumption breakdown
  - Per-resource (agent/workflow) credit consumption breakdown
- **SettingsUsage Page**: Dedicated page for viewing usage analytics
- **GraphQL Query**: `GetBillingAnalytics` query with generated hooks
- **Navigation**: Added Usage menu item in settings (feature-flagged)
- **Mock Data**: Included screenshot mock data for preview/testing

### Feature Flag
- Added `IS_USAGE_ANALYTICS_ENABLED` feature flag to control visibility
and access to analytics features

## Implementation Details
- Analytics data is queried in parallel for performance
- ClickHouse writes are non-blocking to ensure billing operations never
fail
- Progress bars use dynamic coloring from a predefined palette
- Time series visualization normalizes bar heights relative to max value
- Empty state handling when no analytics data is available
- Responsive UI with proper text truncation for long names

https://claude.ai/code/session_01Y1EqrX6PFq3EJxJq89h7DF

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-23 10:28:23 +01:00
Félix MalfaitandGitHub 49af539032 Fix migration crash: agent.modelId NOT NULL violation (#18844)
## Summary

- The `MigrateModelIdsToCompositeFormat` migration was setting
`agent.modelId = NULL`, but the column has a `NOT NULL` constraint —
causing the migration to fail on deploy.
- Fixed by setting `modelId` to the `'default-smart-model'` sentinel
value instead of NULL. The runtime already resolves this sentinel
dynamically via `getEffectiveModelConfig` / `isDefaultModelSentinel`, so
agents correctly fall back to the admin-configured smart model.

## Test plan

- [ ] Run `npx nx run twenty-server:database:migrate:prod` — migration
should complete without errors
- [ ] Verify agents still resolve to the correct model at runtime
(sentinel → `getDefaultPerformanceModel()`)


Made with [Cursor](https://cursor.com)
2026-03-23 09:44:41 +01:00
68a508a353 i18n - translations (#18839)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-23 01:40:20 +01:00
be89ef30cd Migrate field widgets to backend (#18808)
- Renamed FieldConfiguration's layout field to fieldDisplayMode as it
caused issues with the layout field of BarChartConfiguration
- Create relation Field widgets for standard objects

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-23 01:26:00 +01:00
d90d2e3151 i18n - translations (#18838)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-23 00:51:03 +01:00
e9aa6f47e5 Allow users to create Fields and Field widget (#18801)
- Allow users to create a Fields widget or a Field widget; **this PR is
focused on Fields widgets as Field widget can't be configured yet**
- Automatically create a filled view when the user creates a draft
Fields widget in edit mode


https://github.com/user-attachments/assets/b2dbba52-c614-44cd-bf6c-095ce9d4ec26

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-23 00:44:49 +01:00
dc00701448 Fix: TypeError: Cannot convert undefined or null to object (#18333)
## Automated fix for [bug 6848](https://sonarly.com/issue/6848?type=bug)

**Severity:** `critical`

### Summary
WorkflowEditActionCodeFields.tsx calls Object.entries(functionInput)
without a null guard on line 40, crashing when
action.settings.input.logicFunctionInput is undefined. This happens
because workflow version step data in the database may contain the old
field name serverlessFunctionInput (from before the rename in commit
da6f1bbef3), and no data migration was created to update JSON keys
inside the steps JSONB column.

### User Impact
Users viewing or editing a workflow version containing a CODE step that
was created before the serverlessFunction-to-logicFunction rename see a
crash. The page fails to render the workflow step detail panel,
preventing them from viewing or modifying their workflow.

### Root Cause
Proximate cause: Object.entries(functionInput) on line 40 of
WorkflowEditActionCodeFields.tsx throws TypeError because functionInput
is undefined. The stack trace confirms this: the crash occurs during
React rendering (through scheduler -> react-dom render pipeline ->
WorkflowEditActionCodeFields:40:15 -> Object.entries).

1. Why did Object.entries throw? Because the functionInput prop is
undefined. It is initialized from
action.settings.input.logicFunctionInput via useState on line 142 of
WorkflowEditActionCode.tsx, with no fallback value.

2. Why is action.settings.input.logicFunctionInput undefined? Because
the workflow version step data stored in the database JSONB steps column
contains the OLD field name serverlessFunctionInput instead of
logicFunctionInput. When the frontend accesses logicFunctionInput on the
deserialized JSON object, it returns undefined.

3. Why does the database still have the old field name? Because commit
da6f1bbef3 (Rename serverlessFunction to logicFunction, Jan 28 2026)
renamed the Zod schema field from serverlessFunctionInput to
logicFunctionInput, and the database migration
1769556947746-renameServerless.ts only renames SQL tables and columns
(serverlessFunction table to logicFunction, etc.) but does NOT update
the JSON keys inside the steps JSONB column of the workflowVersion
table.

4. Why was no JSON data migration created? The rename was a large-scale
refactoring across the entire codebase. The steps column stores workflow
action data as opaque JSON, and the migration only addressed relational
schema changes (table/column renames) without updating the embedded JSON
document structure. There is no upgrade command in 1-17, 1-18, or 1-19
directories that transforms the JSON keys from serverlessFunctionInput
to logicFunctionInput.

5. Why did the component not handle this gracefully? The
WorkflowEditActionCodeFields component has no null guard on the
functionInput prop before calling Object.entries. Notably, the analogous
WorkflowEditActionLogicFunction component DOES have a null guard on line
52 (action.settings.input.logicFunctionInput ?? {}), showing the team
was aware of this possibility in one component but missed it in the
other.

**Introduced by:** martmull on 2026-02-16 in commit
[`da064d5`](https://github.com/twentyhq/twenty/commit/da064d5e88a62939e0545a37c68381822e6932ef)

### Suggested Fix
Two minimal null guards are added, matching the pattern already used in
the analogous WorkflowEditActionLogicFunction component. First, in
WorkflowEditActionCode.tsx the useState initializer is changed from
action.settings.input.logicFunctionInput to
action.settings.input.logicFunctionInput ?? {} so that functionInput is
always an empty object rather than undefined when the DB record still
uses the old serverlessFunctionInput key. Second, in
WorkflowEditActionCodeFields.tsx the Object.entries call is changed from
Object.entries(functionInput) to Object.entries(functionInput ?? {}) as
a defensive guard at the render layer, ensuring the component renders
safely even if an undefined value is passed from any caller.

### Evidence
- **Code:**
[packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCode.tsx:142
- const [functionInput, setFunctionInput]
=](https://github.com/twentyhq/twenty/blob/main/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCode.tsx#L142)
- **Code:**
[packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCodeFields.tsx:40
- {Object.entries(functionInput ?? {}).map(([inputKey, inputValue]) =>
{](https://github.com/twentyhq/twenty/blob/main/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCodeFields.tsx#L40)

---
*Generated by [Sonarly](https://sonarly.com)*

Co-authored-by: Sonarly Claude Code <claude-code@sonarly.com>
2026-03-23 00:44:19 +01:00
a1b1622d94 i18n - translations (#18837)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-23 00:03:35 +01:00
c107d804d2 Create command menu items for workflows with manual trigger (#18746)
- Automatically create and sync command menu items for all workflows
with a manual trigger
- Refactor `useCommandMenuItemsFromBackend`
- Prefill a _Quick Lead_ workflow command menu item during workspace
setup and dev seeding
- Add a ready prop to `HeadlessEngineCommandWrapperEffect` to prevent
premature execution when async data hasn't loaded yet

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-22 23:52:43 +01:00
d16b94bde6 fix: reset form defaultValues after save to fix dirty detection (#18835)
fixes #18833 
I have put some log video and explained the details in pr #18630.
<img width="929" height="454" alt="image"
src="https://github.com/user-attachments/assets/894d0b89-fb0c-478e-ba42-a4c004a98550"
/>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-22 21:11:02 +00:00
Charles BochetandGitHub e95adcc757 fix(helm): add unit tests, extraEnv schema validation, and minor fixes (#18836)
## Summary

Follow-up to #18157. Cherry-picks the useful parts from #18481 (by
@dnplkndll), adapted to align with the current state of `main`:

- **Helm unit tests** for Redis external authentication (secret-based +
plaintext password, both server and worker)
- **Helm unit tests** for `extraEnv` injection (plain values and
`valueFrom` on both server and worker)
- **JSON schema validation** for `server.extraEnv` and `worker.extraEnv`
in `values.schema.json`
- **Fix** `server_url_test.yaml` to use JSONPath filters
(`@.name=="SERVER_URL"`) instead of brittle `env[0]` index selectors
- **Fix** worker `storageEnv` whitespace (missing `-` in `{{-
$storageEnv | nindent 12 }}`)

Stale tests from #18481 (for `disableDbMigrations`, `server.env`
pass-through, and `run-migrations` init container) were dropped since
those features were removed during the #18157 cleanup.

## Test plan

- [ ] `helm lint` passes
- [ ] `helm template` renders cleanly
- [ ] Unit tests pass with `helm unittest` (requires the plugin)


Made with [Cursor](https://cursor.com)
2026-03-22 21:45:31 +01:00
9d613dc19d Improve helm chart // Fix linting issues & introduce Redis externalSecret for redis password // Add additional ENVs // Improve migrations (#18157)
This pull request enhances the Helm chart for the Twenty application by
improving how environment variables and Redis credentials are handled
for both server and worker deployments. The main changes include support
for injecting additional environment variables, improved Redis password
management (including external secrets), and a more robust database
migration workflow.

**Environment Variable Injection:**
- Added support for specifying additional environment variables for both
the server and worker deployments via the `additionalEnv` field in
`values.yaml`. These variables are automatically injected into the
respective pods.
[[1]](diffhunk://#diff-b5d958eae48fd1919e5623bcf0144aac7abb323ae8743e6f31367e383c63c296R55)
[[2]](diffhunk://#diff-b5d958eae48fd1919e5623bcf0144aac7abb323ae8743e6f31367e383c63c296R109-R110)
[[3]](diffhunk://#diff-20bb91909627a12b50b3c165a2a027b663479c0104ed8dbf91d2b9ad8ea8a931R74-R77)
[[4]](diffhunk://#diff-20bb91909627a12b50b3c165a2a027b663479c0104ed8dbf91d2b9ad8ea8a931R157-R172)
[[5]](diffhunk://#diff-20bb91909627a12b50b3c165a2a027b663479c0104ed8dbf91d2b9ad8ea8a931R225-R229)
[[6]](diffhunk://#diff-fb612a3b7a13156aaa607b27d23025e2c6831f111b6a582fd313fad26d2fdb5bR89-R92)

**Redis Credential Management:**
- Introduced support for using external secrets for Redis passwords by
adding `secretName` and `passwordKey` fields under `redis.external` in
`values.yaml`, and logic to inject `REDIS_PASSWORD` from a Kubernetes
secret if configured.
[[1]](diffhunk://#diff-b5d958eae48fd1919e5623bcf0144aac7abb323ae8743e6f31367e383c63c296R180-R182)
[[2]](diffhunk://#diff-5c4fa358b10abd7581188995feb9b4d6be0bc4f06a95bf27bb31b5595d6693d8R92-R100)
[[3]](diffhunk://#diff-20bb91909627a12b50b3c165a2a027b663479c0104ed8dbf91d2b9ad8ea8a931R157-R172)
[[4]](diffhunk://#diff-20bb91909627a12b50b3c165a2a027b663479c0104ed8dbf91d2b9ad8ea8a931R196-R205)
[[5]](diffhunk://#diff-fb612a3b7a13156aaa607b27d23025e2c6831f111b6a582fd313fad26d2fdb5bR70-R79)
- Updated the logic for constructing the `REDIS_URL` to include
authentication information if a password is set or an external secret is
used.

**Database Migration Workflow:**
- Improved the startup command for the server deployment to optionally
skip database migrations (using `DISABLE_DB_MIGRATIONS`), check for an
existing schema before running migrations, and ensure setup scripts are
only run on empty databases.

These changes make the chart more flexible and secure, especially for
production deployments requiring externalized secrets and custom
environment configurations.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-22 21:36:10 +01:00
23fb2a0d58 i18n - docs translations (#18832)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-22 21:22:28 +01:00
fa04e7077e i18n - translations (#18831)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-22 17:02:40 +01:00
1a0588d233 fix: auto-retry Microsoft OAuth on AADSTS650051 race condition (#18405)
## Summary

Fixes #13008

Azure AD has a [known
bug](https://learn.microsoft.com/en-us/answers/questions/5570899/our-adminconsent-url-always-fails-with-aadsts65005)
where the first consent attempt for a multi-tenant app fails with
`AADSTS650051` because the service principal is mid-provisioning in the
target tenant. Microsoft's own engineers have acknowledged the issue
with no fix ETA.

This PR adds a transparent retry to the `MicrosoftOAuthGuard`:

1. When the OAuth redirect returns `AADSTS650051`, the guard parses the
`state` parameter to recover the original query params (workspaceId,
invite hash, locale, etc.)
2. Redirects the user back to `/auth/microsoft` with a `msRetry=1`
counter
3. The full OAuth flow restarts — by this point the service principal
has finished provisioning, so consent succeeds
4. Capped at 1 retry (`MAX_MICROSOFT_AUTH_RETRIES`) to prevent infinite
loops. If it still fails, falls through to the normal error page

From the user's perspective, they just see a brief extra redirect
instead of a cryptic error page.

### Context

- `AADSTS650051` is a race condition in Azure AD's service principal
provisioning during multi-tenant app consent
- It's distinct from missing consent (`AADSTS65001`) or admin consent
required (`AADSTS90094`)
- The error is transient — retrying the same flow immediately succeeds
- Microsoft Q&A threads confirm this affects many multi-tenant apps, not
just Twenty

### References

- [Microsoft Q&A: adminconsent always fails with AADSTS650051 on first
attempt](https://learn.microsoft.com/en-us/answers/questions/5570899/our-adminconsent-url-always-fails-with-aadsts65005)
- [Microsoft Q&A: AADSTS650051 for multiple
applications](https://learn.microsoft.com/en-us/answers/questions/5571098/when-customers-attempt-to-sign-in-and-grant-consen)

## Test plan

- [ ] Deploy to a staging environment with Microsoft OAuth enabled
- [ ] Have a user from a new Azure AD tenant attempt Microsoft sign-in
for the first time
- [ ] If AADSTS650051 occurs, verify the user is transparently retried
and signs in successfully
- [ ] Verify `msRetry` counter prevents infinite redirect loops (check
server logs for the warn message)
- [ ] Verify normal Microsoft sign-in flow (no error) is unaffected

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: neo773 <62795688+neo773@users.noreply.github.com>
Co-authored-by: neo773 <neo773@protonmail.com>
2026-03-22 16:51:10 +01:00
8ef32c4781 Add In-Reply-To to Email Workflow Node (#18641)
Co-authored-by: github-actions <github-actions@twenty.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-22 16:49:08 +01:00
7bde8a4dfa [Chore]: Migration to dynamic view names for standard views (#18701)
Follow up for #18700

---------

Co-authored-by: Arun kumar <arunkumar@Aruns-MacBook-Air.local>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-22 15:18:17 +00:00
Charles BochetandGitHub d5a7dec117 refactor: rename ObjectMetadataItem to EnrichedObjectMetadataItem and clean up metadata flows (#18830)
## Summary

- Renames `ObjectMetadataItem` to `EnrichedObjectMetadataItem` across
the entire frontend (~440 files) to clarify that this type includes
derived fields (`readableFields`, `updatableFields`, nested `fields[]`,
`indexMetadatas[]`) computed at read time from the metadata store
- Creates `splitObjectMetadataGqlResponse` that goes directly from a
GraphQL `ObjectMetadataItemsQuery` response to flat store items
(combining the old
`mapPaginatedObjectMetadataItemsToObjectMetadataItems` +
`splitObjectMetadataItemWithRelated` two-step flow into one call)
- Removes `ObjectMetadataItemWithRelated` type and all "WithRelated"
naming
- Renames `generatedMockObjectMetadataItems` to
`generateTestEnrichedObjectMetadataItemsMock` to make it clear this is
test-only enriched data
- Deletes `useLoadMockedObjectMetadataItems` hook (consolidated into
`useLoadMockedMinimalMetadata`)
- Ensures nothing destined for the metadata store computes
`readableFields`/`updatableFields` (preventing the localStorage bloat
from #18809)

## Type hierarchy (before → after)

**Before:**
```
ObjectMetadataItemsQuery → mapPaginated → ObjectMetadataItemWithRelated → enrich → ObjectMetadataItem
                                        → split → FlatObjectMetadataItem (store)
```

**After:**
```
ObjectMetadataItemsQuery → splitObjectMetadataGqlResponse → FlatObjectMetadataItem (store)
                         → mapPaginated + enrich (tests only) → EnrichedObjectMetadataItem
```

## Test plan

- [x] `npx nx typecheck twenty-front` passes
- [x] `npx nx test twenty-front` passes (767 suites, 4505 tests)
- [x] `npx nx lint twenty-front` passes
- [ ] CI checks pass


Made with [Cursor](https://cursor.com)
2026-03-22 15:53:47 +01:00
96c5728ed0 fix: prevent localStorage bloat from derived fields on mock metadata (#18809)
## Summary

- On unauthenticated pages (login), mocked object metadata was being
hydrated into the store with `readableFields` and `updatableFields`
attached. These derived arrays duplicate every field per object,
inflating `objectMetadataItems` in localStorage from ~70 KB to ~2 MB.
Combined with other metadata keys, this exceeded Safari's 5 MB quota and
caused `QuotaExceededError`, preventing real data from replacing mock
data on login.
- Extracted flat mock data into a dedicated file
(`generatedMockObjectMetadataItemsWithRelated.ts`) and use it for store
hydration, keeping the enriched version
(`generatedMockObjectMetadataItems`) only for tests.
- Completed `splitObjectMetadataItemWithRelated`'s contract by stripping
`readableFields` and `updatableFields` at runtime (not just via
TypeScript's `Omit`), matching the `FlatObjectMetadataItem` return type
that omits all four relational properties.

## Root cause

1. `MinimalMetadataLoadEffect` loads mocked metadata on unauthenticated
pages.
2. `generatedMockObjectMetadataItems` was produced by
`enrichObjectMetadataItemsWithPermissions`, which attaches
`readableFields` and `updatableFields` (full copies of the `fields`
array).
3. `splitObjectMetadataItemWithRelated` only destructured `fields` and
`indexMetadatas` — `readableFields`/`updatableFields` leaked into
`...objectProperties` at runtime because `Omit` is a compile-time-only
guard.
4. These bloated objects were written to localStorage, consuming ~2 MB
instead of ~70 KB.
5. On login, the intermediate state (old composite mock `current` + new
flat real `draft`) exceeded the 5 MB quota, causing a
`QuotaExceededError` deadlock.

## Test plan

- [ ] Open the app in a fresh Safari private window (empty localStorage)
- [ ] Verify the login page loads without errors
- [ ] Log in and verify metadata loads correctly
- [ ] Check `localStorage` size —
`metadataStoreState__objectMetadataItems` should be ~70 KB, not ~2 MB
- [ ] Run existing tests: `npx nx test twenty-front` — no regressions
from the mock data split


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-22 13:54:44 +01:00
bdaff0b7e2 Fetch load more on group by (#18811)
Fixes https://github.com/twentyhq/twenty/issues/18587 "Load More" in
grouped record table view (aggregated view) which was broken — clicking
it loaded no records and made the button disappear

Root cause: the Load More button called fetchMore on a useLazyQuery that
was never executed, causing Apollo to throw Invariant Violation:
'fetchMore' cannot be called before executing the query. The component
now uses fetchMoreRecords from the same useQuery that powers the table
data.

Before

https://github.com/user-attachments/assets/5266424b-7f35-4261-b7b7-9f7bc3eb8ad6

After

https://github.com/user-attachments/assets/866fe7d7-720a-40c7-bb28-267b85bd98d5

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-22 12:59:09 +01:00
Charles BochetandGitHub 9a306ddb9a feat: store SSO connections as connected accounts during sign-in (#18825)
## Summary

- Store SSO connections (Google, Microsoft, OIDC, SAML) as connected
accounts in the core schema during sign-in/sign-up, gated behind the
`IS_CONNECTED_ACCOUNT_MIGRATED` feature flag
- Add `OIDC` and `SAML` to `ConnectedAccountProvider` enum with
exhaustive switch handling across frontend and backend
- Add `IS_CONNECTED_ACCOUNT_MIGRATED` to `DEFAULT_FEATURE_FLAGS` for new
workspaces, with a fallback check so SSO accounts are created even
before workspace activation
- Always upsert connected accounts to both workspace and core schemas
during messaging OAuth flow, fixing FK constraint violations when
SSO-only accounts exist only in core
- Create message/calendar channels when they don't exist regardless of
new vs reconnect flow
- Filter settings accounts list to only show accounts that have message
or calendar channels

## Test plan

- [ ] Sign up with Google SSO → verify connected account is created in
core schema
- [ ] Connect messaging (Google APIs) after SSO sign-up → verify no FK
errors, channels created, configuration page renders correctly
- [ ] Reconnect an existing messaging account → verify tokens updated,
sync resets triggered
- [ ] Sign in with OIDC/SAML SSO → verify connected account created with
oidcTokenClaims
- [ ] Verify settings accounts page only shows accounts with channels
(SSO-only accounts hidden)
- [ ] Verify typecheck, lint, and unit tests pass
2026-03-22 12:29:58 +01:00
neo773andGitHub 246afe0f2a fix: skip chat messages query when thread ID is the unknown-thread default (#18828)
Prevents invalid UUID error on initial mount before a real thread is
selected
2026-03-22 10:53:43 +01:00
d69e4d7008 fix: prevent FIND_RECORDS from silently dropping unresolved filter variables (#18814)
## Summary

Fixes #18744 — The workflow FIND_RECORDS action silently drops filter
conditions when a variable resolves to null/empty, causing the query to
return **all records** instead of erroring.

**Root cause (three compounding layers):**

1. **`variable-resolver.ts`** — `resolveString` returns `undefined` when
a variable lookup fails (e.g., `{{steps.trigger.output.userId}}` where
`userId` doesn't exist in context). The return type says `string` but
`evalFromContext` actually returns `undefined` at runtime.

2. **`checkIfShouldSkipFiltering.ts`** — Treats `undefined`/`null`/`""`
values as "skip this filter." This is correct for the **UI filter
builder** (user hasn't finished typing), but wrong for **workflow
execution** (variable resolution failed = misconfigured workflow).

3. **`find-records.workflow-action.ts`** — When all filters are silently
skipped, `computeRecordGqlOperationFilter` returns `{}` (match
everything). The query runs with no filter, returning all records —
silently succeeding with wrong results.

## Fix

Added validation in `find-records.workflow-action.ts` **after**
`resolveInput` but **before** `computeRecordGqlOperationFilter`. For
each filter with a value-requiring operand (i.e., not IS_EMPTY,
IS_NOT_EMPTY, IS_IN_PAST, IS_IN_FUTURE, IS_TODAY), if the resolved value
is `undefined`, `null`, or `""`, throw `INVALID_STEP_INPUT` with a
descriptive error message.

**Why this approach:**
- Scoped to the workflow executor — does **not** break the UI filter
builder's intentional skip-on-empty behavior
- Does not change shared utilities (`checkIfShouldSkipFiltering`,
`resolveInput`) used across the app
- Fails fast with a clear error instead of silently returning wrong data
- 1 file changed, 23 lines added

## Test plan

- [x] Backend typecheck passes
- [x] oxlint passes (0 warnings, 0 errors)
- [x] Prettier passes
- [ ] Manual: Create a workflow with FIND_RECORDS using a variable that
doesn't exist → should error with "Filter condition has an empty value
after variable resolution" instead of returning all records
- [ ] Manual: Create a workflow with FIND_RECORDS using IS_EMPTY operand
(no value needed) → should still work correctly

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-21 18:27:01 +01:00
Charles BochetandGitHub 03a2abb305 fix: board view loads all records instead of showing skeleton placeholders (#18824)
## Summary

- The record board (Kanban view) only loaded the first 10 records per
column, then showed skeleton placeholder cards for the rest without ever
fetching the remaining data
- **Root cause**: The `RecordBoardFetchMoreInViewTriggerComponent`
(IntersectionObserver) is positioned at the bottom of the entire board.
With 10 real cards + 10 skeleton cards per column (~3500px of content),
the trigger div was pushed far beyond the 1600px `rootMargin` detection
zone, so `recordBoardShouldFetchMore` never became `true` and fetch-more
was never triggered
- **Fix**: The initial query now sets `recordBoardShouldFetchMore =
true` when columns need more data, and `triggerRecordBoardFetchMore`
uses an internal `while` loop to load all remaining pages in a single
invocation — making it immune to the InView component racing to reset
the flag between React render cycles
2026-03-21 17:20:53 +01:00
c01fc3bafb i18n - translations (#18823)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-21 16:21:41 +01:00
Félix MalfaitandGitHub 908aefe7c1 feat: replace hardcoded AI model constants with JSON seed catalog (#18818)
## Summary

- Replaces per-provider TypeScript constant files
(`openai-models.const.ts`, `anthropic-models.const.ts`, etc.) with a
single `ai-providers.json` catalog as the source of truth
- Adds runtime model discovery via AI SDK for self-hosted providers,
with `models.dev` enrichment for pricing/capabilities
- Introduces composite model IDs (`provider/modelId`) for canonical,
conflict-free identification
- Simplifies provider configuration: API keys are injected from
environment variables (e.g., `OPENAI_API_KEY`)
- Adds admin panel UI for provider management (add/remove/test), model
discovery, recommended model configuration, and default fast/smart model
selection per workspace
- Removes deprecated config variables (`AI_DISABLED_MODEL_IDS`,
`AUTO_ENABLE_NEW_AI_MODELS`, etc.)
- Adds database migration for composite model ID format

## Test plan

- [ ] Server typecheck passes
- [ ] Frontend typecheck passes
- [ ] Server unit tests pass
- [ ] Frontend unit tests pass
- [ ] CI pipeline green
- [ ] Admin panel AI tab loads correctly
- [ ] Provider discovery works for configured providers
- [ ] Model recommendation toggles persist
- [ ] Default fast/smart model selection works


Made with [Cursor](https://cursor.com)
2026-03-21 16:03:58 +01:00
50a0bef0e4 i18n - translations (#18822)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-21 16:03:02 +01:00
cd651f57cb fix: prevent blank subdomain from being saved (#18812)
## Summary

Fixes #17941 — Saving a blank subdomain causes a redirect to
`.website.com`, effectively breaking the workspace.

**Root cause:** Three layers all fail to reject an empty string `""`:

1. **Frontend (`SettingsDomain.tsx`):** `SaveButton` has both
`onClick={onSave}` and `type="submit"`. The `onClick` fires first,
calling `handleSave()` directly without running Zod validation. So
`isDefined("")` returns `true`, the confirmation modal opens, and the
blank subdomain is submitted.

2. **Backend DTO (`update-workspace-input.ts`):** The `subdomain` field
has `@IsString()` + `@IsOptional()` but no pattern validation, so an
empty string passes the DTO layer.

3. **Backend service (`workspace.service.ts:152`):** `if
(payload.subdomain && ...)` — empty string is falsy in JS, so it skips
`validateSubdomainOrThrow()` entirely and writes `subdomain: ""` to the
database.

**The crash:** After save, the redirect logic does
`"myworkspace.website.com".replace("myworkspace", "")` →
`".website.com"`, sending the user to an invalid URL.

## Fix

- **Frontend:** Call `form.trigger()` at the start of `handleSave` to
run Zod validation regardless of whether the function was invoked via
`onClick` or `form.handleSubmit`. Returns early with validation error if
invalid.
- **Backend DTO:** Add `@Matches(/^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$/)`
to reject invalid subdomains at the request validation layer
(defense-in-depth).
- **Backend service:** Change `if (payload.subdomain && ...)` to `if
(isDefined(payload.subdomain) && ...)` so empty strings route through
`validateSubdomainOrThrow()` instead of being silently skipped.

## Test plan

- [x] Existing `is-subdomain-valid.util.spec.ts` tests pass (36/36)
- [x] TypeScript type checks pass for both `twenty-server` and
`twenty-front`
- [x] oxlint passes on all changed files
- [x] Prettier passes on all changed files
- [ ] Manual: Navigate to Settings > Domains, clear the subdomain field,
click Save — should show validation error, not redirect

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-03-21 14:49:21 +00:00
fc9723949b Fix AI chat re-renders and refactored code (#18585)
This PR: 

- Breaks useAgentChatData into focused effect components (streaming,
fetch,
init, auto-scroll, diff sync)
- Splits message list into non-last (stable) + last (streaming/error) to
prevent full re-renders on each stream chunk
- Adds scroll-to-bottom button and MutationObserver-based auto-scroll on
thread switch
- Lifts loading state from context to atoms
- Adds areEqual to selector
factories

We could improve further but this sets up a robust architecture for
further refactoring.

## Messages flow

The flow of messages loading and streaming is now more solid. 

Everything goes out from `AgentChatAiSdkStreamEffect`, whether loaded
from the DB or streaming directly, and every consumers is using only one
atom `agentChatMessagesComponentFamilyState`

## Data sync effect with callbacks new hook 

See
`packages/twenty-front/src/modules/apollo/hooks/useQueryWithCallbacks.ts`
which allows to fix Apollo v4 migration leftovers and is an
implementation of the pattern we talked about with @charlesBochet

We could refine this pattern in another PR.

# Before



https://github.com/user-attachments/assets/84e7a96f-6790-405d-8a73-2dacbf783be5


# After



https://github.com/user-attachments/assets/4c692e3a-2413-4513-abcc-44d0da311203

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-21 12:52:21 +00:00
d389a4341d [Fix]: Vertical bar charts with max data range do not show data up to the top if above max data range (#18748)
fixes issue : #18606 

we have removed the irrelevant use of filters in the backend which
removes the values that are out of max, min range hence modifying the
rawValues.

- Modified relevant tests
- fixed regressions (Horizontal bar charts rightLabel cropping)
- removed dead utility filterByRange code

Before :
<img width="1253" height="722" alt="Screenshot 2026-03-18 at 10 24
14 PM"
src="https://github.com/user-attachments/assets/20adb5bb-0f96-4952-9777-1f1a6ebe03eb"
/>

After:
<img width="865" height="724" alt="Screenshot 2026-03-18 at 11 16 33 PM"
src="https://github.com/user-attachments/assets/b2695d85-ca5e-4923-8f57-8d3c87c8212a"
/>

<img width="1260" height="689" alt="Screenshot 2026-03-19 at 12 16
41 AM"
src="https://github.com/user-attachments/assets/fb25e15f-eec2-445e-9103-280d58664454"
/>

---------

Co-authored-by: Arun kumar <arunkumar@Aruns-MacBook-Air.local>
Co-authored-by: ehconitin <nitinkoche03@gmail.com>
2026-03-20 19:33:21 +00:00
2a1d22d870 Fix page scrolling when layout customization bar is visible (#18802)
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-03-20 18:02:15 +00:00
Charles BochetandGitHub 732aea9121 fix: restore original migration timestamp for messaging infrastructure (#18810)
## Summary
- Restores the original migration timestamp (`1773945207801`) that was
accidentally changed to `1774005903909` during PR #18787
- Keeps the content improvements (default column values) from that PR
intact

## Test plan
- [ ] Verify migration runs correctly with `npx nx run
twenty-server:database:migrate:prod`


Made with [Cursor](https://cursor.com)
2026-03-20 18:14:51 +01:00
42269cc45b fix(links): preserve percent-encoded URLs during normalization (#18792)
## Summary

This preserves percent-encoded payloads when normalizing links fields.

`lowercaseUrlOriginAndRemoveTrailingSlash` was decoding the path and
query string while lowercasing the URL origin. That changes URLs where
encoded payloads are semantically significant, such as Google Maps links
containing `%2F` segments.

Closes #18698.

## Changes

- stop decoding the path/query payload in
`lowercaseUrlOriginAndRemoveTrailingSlash`
- preserve the raw path, query, and hash while still lowercasing the
origin and trimming a trailing slash
- update shared URL normalization tests to assert encoded payloads stay
encoded
- add a server-side regression test covering imported links field
normalization

## Validation

- `corepack yarn jest --config packages/twenty-shared/jest.config.mjs
packages/twenty-shared/src/utils/url/__tests__/lowercaseUrlOriginAndRemoveTrailingSlash.test.ts
--runInBand`
- `corepack yarn jest --config packages/twenty-server/jest.config.mjs
packages/twenty-server/src/engine/core-modules/record-transformer/utils/__tests__/transform-links-value.util.spec.ts
--runInBand`
- `corepack yarn nx test twenty-server --runInBand
--testFile=src/engine/core-modules/record-transformer/utils/__tests__/transform-links-value.util.spec.ts`

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-20 16:56:12 +00:00
1be0f1554f i18n - docs translations (#18807)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-20 17:46:46 +01:00
d149c2a041 i18n - translations (#18806)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-20 17:44:20 +01:00
EtienneandGitHub 9698996771 fix useless auth in sdk gql codegen command (#18805) 2026-03-20 17:22:41 +01:00
9cb21e71fa feat: secure and user-scope metadata resolvers for messaging infrastructure (#18787)
## Summary

Builds on the messaging infrastructure migration (#18784) by securing
and user-scoping all 4 metadata resolvers:

### DTOs secured
- **ConnectedAccountDTO**: `@HideField()` on `accessToken`,
`refreshToken`, `connectionParameters`, `oidcTokenClaims`
- **MessageChannelDTO / CalendarChannelDTO**: `@HideField()` on
`syncCursor`
- **MessageFolderDTO**: `@HideField()` on `syncCursor`, `externalId`
- **UpdateMessageFolderInputUpdates**: stripped to only `isSynced`
(removed `name`, `syncCursor`, `pendingSyncAction`)

### Resolvers user-scoped via `@AuthUserWorkspaceId()`
- `myConnectedAccounts` — returns only the calling user's accounts (no
permission guard)
- `myMessageChannels(connectedAccountId?)` — returns channels for the
user's connected accounts
- `myCalendarChannels(connectedAccountId?)` — same pattern
- `myMessageFolders(messageChannelId?)` — returns folders through the
ownership chain

### Admin-only listing with permission guard
- `connectedAccounts` query retained with
`SettingsPermissionGuard(CONNECTED_ACCOUNTS)` for admin listing of all
workspace accounts

### Unsafe mutations removed
- Removed `createConnectedAccount`, `updateConnectedAccount` (OAuth/IMAP
flows create/refresh tokens server-side)
- Removed `create*`/`delete*` mutations from MessageChannel,
CalendarChannel, MessageFolder (managed by sync engine)

### Update mutations restricted with ownership verification
- `deleteConnectedAccount(id)` — verifies `entity.userWorkspaceId ===
currentUserWorkspaceId`
- `updateMessageChannel` / `updateCalendarChannel` /
`updateMessageFolder` — verify ownership through connected account chain
- New `OWNERSHIP_VIOLATION` exception codes map to `ForbiddenError` in
GraphQL

### `@AuthUserWorkspaceId` decorator hardened
- Added `allowUndefined` option (default: `false`) — throws
`ForbiddenException` if `userWorkspaceId` is undefined (e.g. API key
auth)
- Existing callers updated to `@AuthUserWorkspaceId({ allowUndefined:
true })` where needed
- New user-scoped resolvers enforce non-undefined `userWorkspaceId` at
decorator level

### Exception handler chaining
- `MessageFolderGraphqlApiExceptionInterceptor`,
`MessageChannelGraphqlApiExceptionInterceptor`,
`CalendarChannelGraphqlApiExceptionInterceptor` chain upstream exception
handling (ConnectedAccountException, MessageChannelException) for
correct `ForbiddenError` propagation

### Metadata services enhanced
- `findByUserWorkspaceId()`, `getUserConnectedAccountIds()`,
`findByConnectedAccountIds()`, `findByMessageChannelIds()`
- `findBy*ForUser()` methods encapsulate ownership checks before
querying
- `verifyOwnership()` on all 4 services with proper chain validation
- Named parameters throughout for clarity

### Dev seeds for both schemas
- Added JANE to connected account, message channel, calendar channel
workspace seeds
- Created message folder workspace seeds (TIM, JONY, JANE)
- New `seed-metadata-entities.util.ts` seeds core schema tables
(connectedAccount, messageChannel, calendarChannel, messageFolder) with
same IDs as workspace seeds, mapping `accountOwnerId` →
`userWorkspaceId`

### Integration tests (using seeds, not raw SQL)
- 4 test suites (`connected-account`, `message-channel`,
`calendar-channel`, `message-folder`)
- Tests use seeded data IDs from seed constants — no raw SQL
inserts/deletes
- Tests read via GraphQL resolvers
- Tests cover: user scoping, admin permission checks, sensitive field
exclusion, ownership enforcement on mutations

### Frontend migration
- Feature-flag-gated hooks (`useMyConnectedAccounts`,
`useMyMessageChannels`, `useMyCalendarChannels`, `useMyMessageFolders`)
- When `IS_CONNECTED_ACCOUNT_MIGRATED` is on: hooks use metadata API
(`POST /metadata`)
- When flag is off: hooks use existing workspace API (`POST /graphql`,
current behavior)
- Settings account pages updated to use new hooks
- `useEffect` extracted to
`SettingsAccountsSelectedMessageChannelEffect` component per project
conventions
- Error messages translated with Lingui

## Test plan
- [x] Server typecheck passes
- [x] Server lint passes
- [x] Server unit tests pass (477 suites, 4269 tests)
- [x] Frontend typecheck passes
- [x] Frontend lint passes
- [x] Integration tests verify user-scoping, ownership enforcement,
hidden fields
- [ ] CI green

---------

Co-authored-by: neo773 <neo773@protonmail.com>
2026-03-20 17:22:22 +01:00
Thomas TrompetteandGitHub a8625d8bfb Clean workflow query invalid input errors from sentry (#18804)
As title
2026-03-20 16:12:25 +00:00
Charles BochetandGitHub 3d49c21b51 fix: decouple viewPicker favorite detection from sorted navigation menu items (#18803)
## Summary

- Reverts the `useSortedNavigationMenuItems` coupling in
`ViewPickerOptionDropdown` introduced by #18791
- The viewPicker now uses `useNavigationMenuItemsData` directly for the
`isFavorite` check, keeping view ordering and navigation menu item
ordering independent

## Why

PR #18791 replaced `useNavigationMenuItemsData` with
`useSortedNavigationMenuItems` in the viewPicker for favorite detection.
While the intent was to filter out stale/orphaned navigation items, this
created an unnecessary coupling: `useSortedNavigationMenuItems`
subscribes to `viewsSelector` and `objectMetadataItemsSelector`, making
the viewPicker transitively dependent on the navigation menu item
ordering system. View ordering in the picker (driven by `view.position`)
and navigation menu item ordering (driven by
`navigationMenuItem.position`) should remain decorrelated.

## Test plan

- [ ] Open the viewPicker dropdown and verify views are listed in
correct order
- [ ] Drag-and-drop to reorder views in the viewPicker — confirm it
works
- [ ] Verify the "Add to Favorite" / "Manage favorite" label still
correctly reflects favorite state
- [ ] Reorder navigation menu items in the sidebar — confirm viewPicker
order is unaffected


Made with [Cursor](https://cursor.com)
2026-03-20 16:08:26 +00:00
7c8f060b08 Fix orphan navigation menu items for deleted views (#18791)
## Summary

Fixes #18757

This fixes a set of Favorites / navigation-menu-item integrity problems
related to deleted views, stale hidden items, and upgraded workspaces
with orphaned navigation items.

## What changed

- delete `navigationMenuItem` entries when their favorited view is
deleted
- keep the client metadata store in sync immediately when a view is
deleted
- determine whether a view is already favorited from visible valid
navigation items instead of raw stale items
- add a `1.20.0` upgrade repair command that deletes orphan navigation
menu items and normalizes positions
- add regression coverage for deletion of both record-based and
view-based navigation menu items

## Details

Server:
- extend `NavigationMenuItemDeletionService` so cleanup applies to
deleted views as well as deleted records
- add regression tests covering record-based deletion, view-based
deletion, and no-op behavior
- add `DeleteOrphanNavigationMenuItemsCommand` to remove orphaned items
pointing to:
  - deleted views
  - deleted records
  - missing folders
- normalize positions per scope (`userWorkspaceId + folderId`) after
repair
- wire the new repair command into the `1.20.0` upgrade flow

Frontend:
- add `useRemoveNavigationMenuItemByViewId`
- remove the related navigation item from client metadata immediately
when deleting a view
- use sorted / visible navigation items for favorite detection so stale
hidden rows do not block re-adding a favorite

## Why

Issue `#18757` reports mismatches between Favorites shown in the UI and
rows users can still find in the database. We found that current
Favorites behavior is driven by `navigationMenuItem`, not the legacy
`favorite` table, and that stale / orphaned `navigationMenuItem` rows
could:
- remain after deleting a favorited view
- stay hidden from the UI if they point to invalid targets
- still cause the UI to think a view was already favorited
- persist in workspaces with migration damage from skipped sequential
upgrades

This patch addresses those cases directly and adds an upgrade-time
repair path for older corrupted workspaces.

## Validation

Passed:
- `./node_modules/.bin/jest --config
packages/twenty-server/jest.config.mjs --runInBand
packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/services/__tests__/navigation-menu-item-deletion.service.spec.ts`
- `./node_modules/.bin/tsc -p packages/twenty-front/tsconfig.json
--noEmit --pretty false`

Known unrelated existing failure:
- `./node_modules/.bin/tsc -p packages/twenty-server/tsconfig.json
--noEmit --pretty false`

The server typecheck failure is pre-existing and unrelated to this
branch. Current errors are around `@file-type/pdf` module resolution and
`is-psl-parsed-domain.type.ts`.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-03-20 15:54:33 +01:00
89300564ba Add a note to documentation about variable change (#18752)
Fixes https://github.com/twentyhq/twenty/issues/18646

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-20 15:54:11 +01:00
e7434bdc39 Direct graphql execution (#18759)
On top of [previous closed
PR](https://github.com/twentyhq/twenty/pull/18713) from @FelixMalfait :
- add a schema-creation-skipping optimization
- extract a handler-per-operation pattern, 
- add runtime input validation guards,
- integrate with the standard workspace cache
- add gql-style error handling


To do/optimize/check : 
- gql parsing and null backfilling


## Intro 

This PR introduces a **direct GraphQL execution path** that bypasses
per-workspace GraphQL schema generation for workspace data queries (CRUD
on user-defined objects like companies, people, tasks, etc.).

## Why

In the current architecture, every workspace gets its own
dynamically-generated GraphQL schema reflecting its custom objects and
fields. This costs **~20MB of RAM per workspace per pod** and takes time
to build. For a multi-tenant SaaS with thousands of workspaces, this is
a significant infrastructure cost and a latency bottleneck (especially
on cold starts or cache misses).

The insight is that most workspace queries (`findMany`, `createOne`,
`updateOne`, etc.) don't actually *need* the full schema — they can be
routed directly to the existing Common API query runners by parsing the
GraphQL AST and matching resolver names against object metadata. The
schema is only truly needed for introspection, subscriptions, or queries
that mix core and workspace resolvers.

## How It Works

1. A Yoga `onRequest` plugin intercepts incoming GraphQL requests
2. It parses the query AST and checks if all top-level fields map to
generated workspace resolvers (e.g. `findManyCompanies`,
`createOnePerson`)
3. If yes, it executes them directly against the query runners, skipping
schema generation entirely
4. If the query contains introspection, subscriptions, or core-only
resolvers, it falls through to the normal path
5. Even for mixed queries it can't fully handle, it sets
`skipWorkspaceSchemaCreation` to avoid building the schema when
unnecessary

The whole thing is gated behind the
`IS_DIRECT_GRAPHQL_EXECUTION_ENABLED` feature flag for safe incremental
rollout.

**Net effect**: dramatically lower memory footprint and faster response
times for the vast majority of workspace API calls.

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
2026-03-20 14:33:56 +00:00
EtienneandGitHub 9a850e2241 fix - handle invoice.paid webhook to recover unpaid subscriptions (#18770)
Fixes https://github.com/twentyhq/private-issues/issues/432

## Problem

When a user's invoice goes unpaid, Stripe moves their subscription to
`unpaid` status, and Twenty suspends the workspace. But if the user pays
that invoice while a new billing period has started, Stripe has already
generated a new **draft** invoice for that period. Since the draft isn't
finalized or paid, Stripe doesn't reactivate the subscription — the
workspace stays suspended indefinitely.

## What was missing

- No handler for the `invoice.paid` Stripe webhook event
- No mechanism to finalize draft invoices that accumulated during the
`unpaid` period
- No way to reset the workspace deletion countdown (`suspendedAt`) when
a user shows payment intent

## What was added

### 1. `StripeInvoiceService` — new Stripe SDK wrapper

- `listDraftInvoices(stripeSubscriptionId)` — lists all draft invoices
for a subscription
- `finalizeInvoice(invoiceId)` — finalizes a draft with `auto_advance:
true` so Stripe auto-charges it

### 2. `INVOICE_PAID` enum value

Added to `BillingWebhookEvent` so the controller can route it.

### 3. `processInvoicePaid()` in `BillingWebhookInvoiceService`

New private handler that:

- Fetches all draft invoices for the subscription
- Filters to only those whose `period_end` is in the past (already
overdue)
- Finalizes each one (with error handling per invoice to avoid blocking
the webhook)
- If the workspace is suspended, resets `suspendedAt` to now (buys time
before deletion)

### 4. Controller routing

`INVOICE_FINALIZED` and `INVOICE_PAID` are now grouped in the same
switch case, both calling `processStripeEvent(data, eventType)`, which
forks internally in the service.

## Expected recovery flow

User pays overdue invoice
→ Stripe fires invoice.paid
→ Handler finalizes past-due draft invoices (auto_advance: true)
→ Stripe auto-charges them
→ Subscription becomes active
→ customer.subscription.updated fires
→ Existing logic unsuspends workspace
→ suspendedAt refreshed (resets deletion countdown while payments
cascade)
2026-03-20 14:19:53 +00:00
Charles BochetandGitHub 91793ef930 fix: update workspace members state on profile onboarding completion (#18799)
## Summary

Fixes #18610

After completing the CreateProfile onboarding step,
`currentWorkspaceMembersState` (plural, used by `useActorFieldDisplay`
to resolve "Created by" names) was never updated with the new name —
only `currentWorkspaceMemberState` (singular) was. This caused "Created
by" fields to display "Untitled" for the remainder of the session.

**Root cause analysis:**

The issue reporter attributed the bug to empty
`core.user.firstName/lastName`, but `createdBy` display doesn't use
`core.user` at all. The actual flow:

1. Sign-up creates workspace member with empty names (copied from empty
`core.user`)
2. `GetCurrentUserDocument` loads `currentWorkspaceMembersState` with
empty names
3. CreateProfile step updates workspace member in DB +
`currentWorkspaceMemberState` (singular) ✓
4. `currentWorkspaceMembersState` (plural) is **never refreshed** — the
query is skipped once `currentUser` exists
5. `useActorFieldDisplay` looks up from the stale plural state → empty
name → "Untitled"

**Fix:** Update `currentWorkspaceMembersState` alongside
`currentWorkspaceMemberState` in the CreateProfile submit handler.
2026-03-20 14:02:47 +00:00
Charles BochetandGitHub 217adc8d17 fix: add missing LEFT JOIN for relation fields in groupBy orderByForRecords (#18798)
## Summary

Fixes "missing FROM-clause entry for table" SQL error when using
`orderByForRecords` with a relation field (e.g. `{ company: { name:
"AscNullsFirst" } }`) in a `groupBy` query.

### Root cause

This is a regression from #18005 (Feb 17). That PR correctly removed a
duplicate `applyOrderToBuilder()` call on the groupBy subquery (which
was conflicting with the `ROW_NUMBER() OVER (... ORDER BY ...)` window
function), but in doing so it also removed the LEFT JOINs that
`applyOrderToBuilder` was adding for relation fields.

After that change, ordering relied solely on `getOrderByRawSQL()` inside
`applyPartitionByToBuilder()`, which builds raw SQL for the window
function but never added the required JOINs. Scalar field ordering (e.g.
`name`, `position`) kept working since those don't need JOINs, but
relation field ordering (e.g. `company.name`) broke.

### Fix

- `getOrderByRawSQL` now returns `relationJoins` alongside the SQL
string so callers get the join info they need
- `applyPartitionByToBuilder` in `GroupByWithRecordsService` adds the
required LEFT JOINs before building the `ROW_NUMBER()` window function,
mirroring the pattern used by `applyOrderToBuilder` in the `findMany`
path

## Test plan

- [x] Manually tested locally with a GraphQL query matching the
customer's failing pattern (`orderByForRecords: [{ company: { name:
"AscNullsFirst" } }]`)
- [x] Added integration tests for ascending and descending relation
field ordering in `group-by-with-records-resolver.integration-spec.ts`
- [x] Typecheck passes
- [x] Lint passes
- [x] CI green
2026-03-20 14:02:35 +00:00
Thomas TrompetteandGitHub 698c15b8cd Avoid side panel tab overriden by show page (#18800)
Missing prop so layout are considered as the same
2026-03-20 13:56:02 +00:00
Charles BochetandGitHub 34b3158308 fix: backfill missing ids on SELECT/MULTI_SELECT field metadata options (#18797)
## Summary
- Some production workspaces have `SELECT` or `MULTI_SELECT`
fieldMetadata options that are missing the `id` property (e.g.
`[{"color":"yellow","label":"Draft","value":"DRAFT","position":0},
...]`).
- Adds a new upgrade command
`upgrade:1-20:backfill-select-field-option-ids` that queries all
SELECT/MULTI_SELECT fieldMetadata per workspace, detects options missing
an `id`, and backfills them with a UUID v4.
- The command is idempotent (no-op when all options already have ids),
supports `--dry-run`, and invalidates workspace caches after patching.
2026-03-20 13:08:55 +00:00
Charles BochetandGitHub 66be7c3ef4 fix: autogrow input sizing + surface nested error details in upgrade command (#18796)
## Summary

### Fix 1: Autogrow input unclickable when value is empty string
- Fixes the last name input on the Person record show page being
unclickable on regular screens

### Fix 2: Surface nested migration errors in add-missing-system-fields
command
- `AddMissingSystemFieldsToStandardObjectsCommand` calls
`workspaceMigrationRunnerService.run()` directly and was re-throwing
`WorkspaceMigrationRunnerException` without reading its nested `errors`
- Extracted `getNestedErrorMessages()` helper (reused by both
`isUniqueViolationError` and `enrichErrorMessage`)
- Both catch sites now call `enrichErrorMessage()` before re-throwing,
appending nested error details to the message

**Before:**
```
ERROR [UpgradeCommand] Error in workspace ...: Migration action 'create' for 'fieldMetadata' failed
ERROR [UpgradeCommand] undefined
```

**After:**
```
ERROR [UpgradeCommand] Error in workspace ...: Migration action 'create' for 'fieldMetadata' failed (metadata: duplicate key value violates unique constraint "...")
```

## Test plan
- [x] Lint passes
- [x] Typecheck passes
- [x] Verify upgrade command errors now include nested error details
- [x] Verify autogrow input is clickable when value is empty string
2026-03-20 12:22:27 +00:00
Félix MalfaitandGitHub 0a6b514898 fix: remove redundant cookie write that made tokenPair a session cookie (#18795)
## Summary

- Removes a redundant direct `cookieStorage.setItem('tokenPair', ...)`
call in `handleSetAuthTokens` that was overwriting the Jotai-managed
cookie (which has a 180-day expiry) with a session cookie (no expiry)
- This caused users to be logged out whenever their browser fully
closed, instead of staying authenticated for 180 days

## Root cause

In April 2025 (`a7e6564017`), a direct `cookieStorage.setItem` call was
added alongside `setTokenPair()` as a workaround because Recoil's
`onSet` effect fired too late for the Apollo client to read the token
synchronously.

In February 2026 (`674f4353cd`), `tokenPairState` was migrated from
Recoil to Jotai's `atomWithStorage`, which writes the cookie
**synchronously** with a 180-day `expires`. The old direct write was
left in place and now runs *after* the Jotai write, overwriting the
cookie without an `expires` — making it a session cookie.

## Test plan

- [ ] Log in to the app
- [ ] Inspect the `tokenPair` cookie in DevTools → Application → Cookies
- [ ] Verify the cookie has an expiration date ~180 days from now (not
"Session")
- [ ] Close and reopen the browser — confirm you remain logged in


Made with [Cursor](https://cursor.com)
2026-03-20 12:03:30 +01:00
Thomas TrompetteandGitHub ffb02a6878 Improve workflow metrics with faillure reason (#18768)
- split workflow errors into system and user errors
- add workspace id to attributes for debugging
2026-03-20 10:04:40 +00:00
3cbd9f2b5d i18n - translations (#18786)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-20 00:39:40 +01:00
Charles BochetandGitHub cee4cf6452 feat: migrate ConnectedAccount infrastructure entities to metadata schema (#18784)
## Summary

- Migrates 4 entities (`connectedAccount`, `messageChannel`,
`calendarChannel`, `messageFolder`) from per-workspace schemas to the
shared `core` metadata schema
- Introduces a `IS_CONNECTED_ACCOUNT_MIGRATED` feature flag to control
the migration: when enabled, reads come from core metadata and all
writes are dual-written to both workspace and core
- Extracts 12 enums from workspace entity files to `twenty-shared` for
reuse across frontend and backend
- Creates new TypeORM entities, metadata services, GraphQL
resolvers/DTOs, and exception interceptors per entity
- Each entity owns its own data access module
(`ConnectedAccountDataAccessModule`, `MessageChannelDataAccessModule`,
`CalendarChannelDataAccessModule`, `MessageFolderDataAccessModule`) — no
umbrella infrastructure module
- Adds a 1.20 upgrade command that backfills data from workspace schemas
to core (preserving UUIDs) and enables the feature flag
- Replaces direct repository access with data access service calls
across ~50 files in messaging, calendar, and connected-account modules
- Adds `lastSignedInAt` and `oidcTokenClaims` fields to the new
`ConnectedAccountEntity`
- Drops unused `lastSyncHistoryId` field from the migrated connected
account entity

## Test plan

- [x] Lint passes (`npx nx lint:diff-with-main twenty-server`)
- [x] Typecheck passes (`npx nx typecheck twenty-server`)
- [x] All unit tests pass (477 suites, 4267 tests, 0 failures)
- [ ] Manual test: verify messaging sync works with feature flag
disabled (existing behavior)
- [ ] Manual test: run upgrade command on a workspace, verify data
backfilled to core tables
- [ ] Manual test: verify messaging/calendar sync works with feature
flag enabled (dual-write path)
- [ ] Manual test: verify GraphQL metadata resolvers return correct data
when flag enabled
2026-03-20 00:34:58 +01:00
Abdul RahmanandGitHub cd594ce8bd Fix: Hide object from Opened section when it already has a workspace nav item (#18766)
Previously, the Opened section was always hidden for all workflow
objects. We now base the “in nav” check on the full set of object
metadata IDs that have a workspace nav item, instead of only active
non-system objects. So: if an object has a nav item (e.g. workflow
runs), it no longer appears under Opened; if it doesn’t, it can appear
there. The hook was simplified to only expose
`objectMetadataIdsInWorkspaceNav`, and the unused return value was
removed.
2026-03-19 21:57:11 +00:00
26139ee463 fix: stop event propagation when removing file in AI chat preview (#18779)
## Summary

When clicking the ✕ button on an uploaded file in the AI chatbot context
preview, the click event bubbled up to the `StyledClickableContainer`
parent, which triggered `handleClick` (opening the file preview modal)
instead of — or in addition to — calling `onRemove`.

**Root cause:** `StyledClickableContainer` has `onClick={handleClick}`
at the div level. The `AvatarOrIcon` (X button) `onClick={onRemove}`
callback didn't stop propagation, so the event continued to bubble and
opened the preview.

**Fix:** Wrap `onRemove` in a `handleRemove` callback that calls
`e.stopPropagation()` before invoking the original handler.

Fixes #18298

---------

Co-authored-by: victorjzq <zhiqiangjia@users.noreply.github.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-03-19 19:07:19 +00:00
8005b35b56 Fix relation connect where failing on mixed-case email and URL (#18605)
Related to issue #17711 
Follow-up to PR #17774  which fixed the frontend link normalization only
## Summary
- Normalize `primaryEmail` (lowercase) and `primaryLinkUrl` in relation
`connect.where` composite values
- Applied in both frontend spreadsheet import and backend
`DataArgProcessorService` to cover all entry points (UI import, GraphQL
API)

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-03-19 17:56:26 +00:00
Abdul RahmanandGitHub 02bfebccc2 Fix: cascade delete favorite folder children on backend (#18765) 2026-03-19 17:55:28 +00:00
Thomas TrompetteandGitHub b86e6189c0 Remove postition from Timeline Activities + fix workflow title placeholder (#18777)
- Position were not properly displayed because we never implemented a
display for this
- Untitled placeholder was not displayed anymore
<img width="359" height="117" alt="Capture d’écran 2026-03-19 à 17 11
25"
src="https://github.com/user-attachments/assets/64c90d81-8262-4176-ae25-804748e36b1e"
/>
2026-03-19 17:25:35 +00:00
a9f8a7e1fa Fix: prevent record navigation when clicking Remove from favorite in nav sidebar (#18760)
https://github.com/user-attachments/assets/96abf04d-726a-4225-846e-e5d701a583a2

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-19 14:29:12 +00:00
5526d2e5d0 Separate metadata and object record publisher (#18740)
- Separate both publishers
- Renaming
- Removal of the old SSE endpoint

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-19 13:24:38 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Charles Bochet
0d27a255a9 chore(deps): bump nodemailer from 7.0.11 to 7.0.13 (#18754)
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 7.0.11
to 7.0.13.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/nodemailer/nodemailer/releases">nodemailer's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.13</h2>
<h2><a
href="https://github.com/nodemailer/nodemailer/compare/v7.0.12...v7.0.13">7.0.13</a>
(2026-01-27)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>downgrade transient connection error logs to warn level (<a
href="https://github.com/nodemailer/nodemailer/commit/4c041db85d560e98bc5e1fd5d5a191835c5b7d2f">4c041db</a>)</li>
</ul>
<h2>v7.0.12</h2>
<h2><a
href="https://github.com/nodemailer/nodemailer/compare/v7.0.11...v7.0.12">7.0.12</a>
(2025-12-22)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>added support for REQUIRETLS (<a
href="https://redirect.github.com/nodemailer/nodemailer/issues/1793">#1793</a>)
(<a
href="https://github.com/nodemailer/nodemailer/commit/053ce6a772a7c608e6bee7f58ebe9900afbd9b84">053ce6a</a>)</li>
<li>use 8bit encoding for message/rfc822 attachments (<a
href="https://github.com/nodemailer/nodemailer/commit/adf86113217b23ff3cd1191af5cd1d360fcc313b">adf8611</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md">nodemailer's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/nodemailer/nodemailer/compare/v7.0.12...v7.0.13">7.0.13</a>
(2026-01-27)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>downgrade transient connection error logs to warn level (<a
href="https://github.com/nodemailer/nodemailer/commit/4c041db85d560e98bc5e1fd5d5a191835c5b7d2f">4c041db</a>)</li>
</ul>
<h2><a
href="https://github.com/nodemailer/nodemailer/compare/v7.0.11...v7.0.12">7.0.12</a>
(2025-12-22)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>added support for REQUIRETLS (<a
href="https://redirect.github.com/nodemailer/nodemailer/issues/1793">#1793</a>)
(<a
href="https://github.com/nodemailer/nodemailer/commit/053ce6a772a7c608e6bee7f58ebe9900afbd9b84">053ce6a</a>)</li>
<li>use 8bit encoding for message/rfc822 attachments (<a
href="https://github.com/nodemailer/nodemailer/commit/adf86113217b23ff3cd1191af5cd1d360fcc313b">adf8611</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/nodemailer/nodemailer/commit/893119505aa25723dd9a7d90c8dfd223d28a0cc4"><code>8931195</code></a>
chore(master): release 7.0.13 (<a
href="https://redirect.github.com/nodemailer/nodemailer/issues/1798">#1798</a>)</li>
<li><a
href="https://github.com/nodemailer/nodemailer/commit/9398d633d2c1bf5bf08a0e258ff17d5d7a45f0e6"><code>9398d63</code></a>
Bumped deps</li>
<li><a
href="https://github.com/nodemailer/nodemailer/commit/4c041db85d560e98bc5e1fd5d5a191835c5b7d2f"><code>4c041db</code></a>
fix: downgrade transient connection error logs to warn level</li>
<li><a
href="https://github.com/nodemailer/nodemailer/commit/a208a0bc86a315d037d5be8849fab5862c488baa"><code>a208a0b</code></a>
chore(master): release 7.0.12 (<a
href="https://redirect.github.com/nodemailer/nodemailer/issues/1785">#1785</a>)</li>
<li><a
href="https://github.com/nodemailer/nodemailer/commit/053ce6a772a7c608e6bee7f58ebe9900afbd9b84"><code>053ce6a</code></a>
fix: added support for REQUIRETLS (<a
href="https://redirect.github.com/nodemailer/nodemailer/issues/1793">#1793</a>)</li>
<li><a
href="https://github.com/nodemailer/nodemailer/commit/adf86113217b23ff3cd1191af5cd1d360fcc313b"><code>adf8611</code></a>
fix: use 8bit encoding for message/rfc822 attachments</li>
<li>See full diff in <a
href="https://github.com/nodemailer/nodemailer/compare/v7.0.11...v7.0.13">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=nodemailer&package-manager=npm_and_yarn&previous-version=7.0.11&new-version=7.0.13)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-03-19 09:22:17 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ef2a113a16 chore(deps): bump @dagrejs/dagre from 1.1.3 to 1.1.8 (#18753)
Bumps [@dagrejs/dagre](https://github.com/dagrejs/dagre) from 1.1.3 to
1.1.8.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/dagrejs/dagre/commit/7e4d15f191678f7f05f3c86d9071a193230e7e00"><code>7e4d15f</code></a>
Building for release</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/d3908e2c13148c9143db585accc10ae0b6634657"><code>d3908e2</code></a>
Bumping version</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/ce295f8e073c4fe96c9e36ecf08ae2940e5e6a10"><code>ce295f8</code></a>
Build for release</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/b64b9057726eee17f24f73579ba0668527276448"><code>b64b905</code></a>
Bumping the version</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/de169d24c13d06c1e9c560f4f4f8f98650109b94"><code>de169d2</code></a>
Merge pull request <a
href="https://redirect.github.com/dagrejs/dagre/issues/481">#481</a>
from Nathan-Fenner/nf/improve-network-simplex-perform...</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/065e0d8374f4c1c35a7cb4b84df37aaa31598d86"><code>065e0d8</code></a>
improve performance of graph node ranking</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/00d3178d671e49de9c032e3abd281dc9f2739e73"><code>00d3178</code></a>
Typo</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/3982a69d2b323b06aa969a4ec09829d37fe6e7bd"><code>3982a69</code></a>
Bump version and set as pre-release</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/1339f5516508dba0cbcc4ef1c0587e7384bec23d"><code>1339f55</code></a>
Building for release</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/9459f01bc815f16b87db727821d8401acbad2cd3"><code>9459f01</code></a>
Bumping the version</li>
<li>Additional commits viewable in <a
href="https://github.com/dagrejs/dagre/compare/v1.1.3...v1.1.8">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@dagrejs/dagre&package-manager=npm_and_yarn&previous-version=1.1.3&new-version=1.1.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-19 09:11:58 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e0e25eac2b chore(deps): bump @ai-sdk/mistral from 3.0.20 to 3.0.25 (#18755)
Bumps [@ai-sdk/mistral](https://github.com/vercel/ai) from 3.0.20 to
3.0.25.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/vercel/ai/commit/4c1613ac8ff9f638da3a25fa6a1aa40362eb0e7c"><code>4c1613a</code></a>
Version Packages (<a
href="https://redirect.github.com/vercel/ai/issues/13131">#13131</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/64ac0fdd80da66d1e920a8a6309ebe05b9d686ad"><code>64ac0fd</code></a>
Backport: fix(security): validate redirect targets in download functions
to p...</li>
<li><a
href="https://github.com/vercel/ai/commit/f622bf85c114f810b00dd2825b56e044bce578a7"><code>f622bf8</code></a>
Version Packages (<a
href="https://redirect.github.com/vercel/ai/issues/13126">#13126</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/e2a59ef9273ef2ad97c8aec5bf5c0a7b9e8c436c"><code>e2a59ef</code></a>
Backport: fix(provider/google): preserve groundingMetadata when streamed
befo...</li>
<li><a
href="https://github.com/vercel/ai/commit/ebf43a6765e6fa337e8d5b6cf065625a1794c21c"><code>ebf43a6</code></a>
Version Packages (<a
href="https://redirect.github.com/vercel/ai/issues/13120">#13120</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/258900473214256af40b035b66f15362cbc53287"><code>2589004</code></a>
Backport: feat(openai): add GPT-5.4 model support (<a
href="https://redirect.github.com/vercel/ai/issues/13117">#13117</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/d23121fd71036611e0142a9233de4d1a1f5d54c0"><code>d23121f</code></a>
Backport: chore(ai): add optional ChatRequestOptions to
`addToolApprovalRespo...</li>
<li><a
href="https://github.com/vercel/ai/commit/55a2acf625c78d5c68ac2abc02bc26c3cd0ffa6e"><code>55a2acf</code></a>
Version Packages (<a
href="https://redirect.github.com/vercel/ai/issues/13101">#13101</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/45d71c381b756e03310f81bdd322aa7460cb5a15"><code>45d71c3</code></a>
Backport: fix(google): use VALIDATED function calling mode when any tool
has ...</li>
<li><a
href="https://github.com/vercel/ai/commit/ee92dc766422fffde3bae06437510296c4e860a8"><code>ee92dc7</code></a>
ci(release): add <code>--tag ai-v6</code> to <code>ci:release</code>
script (<a
href="https://redirect.github.com/vercel/ai/issues/13096">#13096</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/vercel/ai/compare/@ai-sdk/mistral@3.0.20...@ai-sdk/mistral@3.0.25">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@ai-sdk/mistral&package-manager=npm_and_yarn&previous-version=3.0.20&new-version=3.0.25)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-19 09:10:58 +00:00
Charles BochetandGitHub 8ab8f80687 fix: use unique concurrency group per merge queue entry (#18756)
## Summary

- Fixes merge queue PRs blocking each other by changing the concurrency
group in `ci-merge-queue.yaml`
- The old concurrency group used `merge_group.base_ref` which resolves
to `refs/heads/main` for every PR, causing all merge queue entries to
serialize behind a single concurrency slot
- Now uses `github.ref` (unique per entry:
`refs/heads/gh-readonly-queue/main/pr-NUMBER-SHA`), matching what all
other CI workflows already do

## Recommended ruleset changes (in GitHub Settings > Rules > Rulesets >
"CI Status Checks")

- **Grouping strategy**: Switch `ALLGREEN` to `NONE` -- each PR is still
tested against the correct base (including all PRs ahead of it in the
queue), but failures only affect the failing PR instead of ejecting the
entire group. `max_entries_to_build: 5` still allows parallel
speculative testing.
- **`min_entries_to_merge_wait_minutes`**: Reduce from 5 to 1 -- the
5-minute wait adds unnecessary latency to every merge.

## Test plan

- [ ] Enqueue 2+ PRs in the merge queue and verify both trigger e2e
tests in parallel instead of one blocking the other
2026-03-19 10:10:47 +01:00
a370a26b79 i18n - docs translations (#18749)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-19 09:36:37 +01:00
martmullandGitHub 2f095c8903 Scaffold light twenty app dev container (#18734)
as title
2026-03-18 20:10:54 +01:00
Charles BochetandGitHub 394a3cef15 fix: restore ViewFilter value stringification lost in converter removal (#18745)
## Summary

- **Restores `convertViewFilterValueToString()` calls** that were lost
when the converter layer was removed in #18667. The GraphQL
`ViewFilter.value` is typed as `JSON` (can be a string, array, or
object), but the frontend type system expects a `string`. Without
stringification, SELECT/MULTI_SELECT filter values (e.g. `['LOST']`)
reach `arrayOfStringsOrVariablesSchema` as raw arrays, causing a
`ZodError: expected string, received array`.
- **Fixes applied at two data boundary points**: `splitViewWithRelated`
(primary entry from metadata store) and `mapViewFiltersToFilters` (which
also accepts `GqlViewFilter[]` directly).

Fixes a production regression introduced by #18667.

## Test plan

- [ ] Apply a SELECT filter (e.g. filter Opportunities by Stage =
"Lost") — should no longer throw ZodError
- [ ] Apply a MULTI_SELECT filter — should work correctly
- [ ] Verify filters with multiple selected values work (e.g. Stage is
"Lost" or "Won")
- [ ] Verify empty filters and "is not" operands still work
- [ ] Verify filters loaded from saved views still work after page
refresh


Made with [Cursor](https://cursor.com)
2026-03-18 19:06:43 +01:00
189c564840 i18n - translations (#18742)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-18 18:19:45 +01:00
Abdul RahmanandGitHub 6d4d1a97bc Migrate object permission to syncable entity (#18609)
Closes [#2223](https://github.com/twentyhq/core-team-issues/issues/2223)
2026-03-18 16:32:18 +00:00
BugIsGodandGitHub 8ccaf635bc fix: replace > button with button selector in styled wrappers (#18568)
fix CSS selector > button not reaching Button component's inner element
### Reproduce
- when you click Data model and create a new field, you will see this
bug.
- When you import record and check the height of remove button in the
final validation step.

### Root Reason
the Button component internally renders a wrapper div around the
<button> element, so > button only reaches the intermediate div, not the
actual button.

### Fix
- padding-right not applied → chevron overlapped with text
- ValidationStep: height: 24px not applied → Remove button was 32px
instead of 24px

<img width="1288" height="376" alt="image"
src="https://github.com/user-attachments/assets/885cd8b0-1fe2-484a-8425-70f52b784ecb"
/>

<img width="1001" height="291" alt="image"
src="https://github.com/user-attachments/assets/0b489478-8fbc-4f7e-886a-38012eeb07ef"
/>
2026-03-18 15:34:33 +00:00
91d6a69bf8 i18n - translations (#18738)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-18 16:18:52 +01:00
Félix MalfaitandGitHub c6f11d8adb fix: migrate driver modules to DriverFactoryBase lazy-loading pattern (#18731)
## Summary

- Migrates `LogicFunctionModule`, `CodeInterpreterModule`, and
`CaptchaModule` from the `forRootAsync` + injection token pattern to the
`DriverFactoryBase` lazy-loading pattern (matching `EmailModule` and
`FileStorageModule`)
- Fixes #18724 where `LOGIC_FUNCTION_TYPE` was not respected in worker
processes because the driver was created at module boot time before the
DB config cache was loaded
- Removes `isEnvOnly` from `LOGIC_FUNCTION_TYPE`,
`CODE_INTERPRETER_TYPE`, `CAPTCHA_DRIVER`, `IS_MULTIWORKSPACE_ENABLED`,
and `FRONTEND_URL` — these can now be safely configured via the database
at runtime

## How it works

Each migrated module now uses a `DriverFactory` (extending
`DriverFactoryBase`) instead of a module-level async factory + Symbol
injection token:

1. **Lazy creation**: `getCurrentDriver()` creates the driver on first
call, after `DatabaseConfigDriver.onModuleInit()` has loaded the DB
cache
2. **Auto-recreation**: If config changes in the DB, the next
`getCurrentDriver()` call detects the key mismatch and creates a new
driver instance
3. **Unified config**: Both server and worker read from the same
database — driver config only needs to be set once

### Files deleted (old pattern)
- `logic-function-module.factory.ts`,
`logic-function-drivers.module.ts`, `logic-function-driver.constants.ts`
- `code-interpreter-module.factory.ts`
- `captcha.module-factory.ts`, `captcha-driver.constants.ts`

### Files created (new pattern)
- `logic-function-driver.factory.ts`
- `code-interpreter-driver.factory.ts`
- `captcha-driver.factory.ts`

Net: **-150 lines**

## Test plan

- [x] `npx nx typecheck twenty-server` passes
- [x] `npx nx lint:diff-with-main twenty-server` passes
- [ ] Integration tests pass (`npx nx run
twenty-server:test:integration:with-db-reset`)
- [ ] Verify logic functions execute in workflow runs (the original bug)
- [ ] Verify code interpreter works in workflow code steps
- [ ] Verify captcha validation works on sign-up (when captcha is
configured)


Made with [Cursor](https://cursor.com)
2026-03-18 16:00:45 +01:00
fe4512f80c Add record page layout tabs editing (#18702)
- Fix duplication of tabs in dashboards that didn't open the duplicated
tab in the side panel: replaced the logic that used Math.round with an
algorithm that switches the positions of the elements. We will keep
relying on integers, but it will work well in all cases.
- Make dnd work with record page layouts, where there is a pinned tab
- Make tab movements work with pinned tab, too
- Allow user to set a tab as the pinned tab
- Make backend changes to be able to save tabs with `layoutMode`



https://github.com/user-attachments/assets/ce1130fa-71df-49ba-ba2f-6f971e15dd49

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-18 14:48:23 +00:00
Raphaël BosiandGitHub f2fa958f20 Add progress tracking to command menu items (#18732)
Introduces a progress indicator for command menu items (both engine
commands and front components). When a command is running, the menu item
now displays a percentage alongside the loader spinner instead of just a
spinner.

- Added `commandMenuItemProgressFamilyState` to track per-item progress
and `CommandListItemLoader` to render it
- Exposed `updateProgress` in the twenty-sdk public API so front
components can report execution progress back to the host
- Wired progress reporting into `ExportMultipleRecordsCommand` as the
first consumer, showing CSV export progress
- Progress state is cleaned up on unmount for both engine commands and
headless front components
- Refactor
2026-03-18 14:37:27 +00:00
Raphaël BosiandGitHub eb51f8e6da Remove front component and command menu item old seeds (#18737)
Deprecated seeds
2026-03-18 14:07:56 +00:00
Félix MalfaitandGitHub b6c62b3812 fix(auth): use dynamic SSE headers and add token renewal retry logic (#18706)
## Summary

- **Dynamic SSE headers**: The `graphql-sse` client was created with a
static `Authorization` header captured at creation time. When the access
token refreshed, the SSE client kept using the expired token on every
reconnection attempt, causing up to 10 wasted retries before the client
was disposed and recreated. Now `headers` is passed as a function that
reads the latest token from the Jotai store on each connection attempt.

- **Token renewal retry with error classification**:
`handleTokenRenewal` previously had zero retry tolerance — any failure
during `renewToken` (including transient network errors, server 500s, or
timeouts) triggered an immediate full logout via
`onUnauthenticatedError()`. Now the renewal retries up to 3 times with
linear backoff for transient errors. Only explicit server rejections
(`CombinedGraphQLErrors`, e.g. expired/revoked refresh token) skip
retries and proceed to logout immediately.

- **Preserved error types in `renewTokenMutation`**: The old code caught
all errors and re-threw them as a generic `new Error('Something went
wrong...')`, destroying the original error type. Callers couldn't
distinguish a GraphQL auth rejection from a network failure. Now errors
propagate with their original type.

- **Simplified SSE retry handler**: With dynamic headers handling token
freshness automatically, the retry handler no longer needs the
`initialTokenForSseClient` comparison to detect token mismatches. It now
only resets the SSE client when the user has logged out (no token) or
after 10+ consecutive failures.

## Test plan

- [ ] Log in, wait for access token to expire (~30 min or configure
shorter expiry), verify no unexpected logout occurs
- [ ] Simulate transient network failure during token renewal (e.g.
throttle network in devtools), verify the retry logic recovers without
logging out
- [ ] Verify SSE real-time updates continue working after a token
refresh
- [ ] Verify genuine logout still works when refresh token is actually
invalid/expired
- [ ] Open multiple browser tabs, verify token refresh works correctly
across all tabs without "suspicious activity" revocation


Made with [Cursor](https://cursor.com)
2026-03-18 15:08:30 +01:00
c676e46a1d i18n - translations (#18736)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-18 14:48:17 +01:00
Abdul RahmanandGitHub c7e89a18f0 Migrate permission flag to syncable entity (#18567)
Closes [#2225](https://github.com/twentyhq/core-team-issues/issues/2225)
2026-03-18 13:25:52 +00:00
Charles BochetandGitHub 58336fb70f fix: navigation menu item type backfill and frontend loading (#18730)
## Summary

- Move `BackfillNavigationMenuItemTypeCommand` from the 1-19 to the 1-20
upgrade path and split the DB transaction into two phases (data
backfill, then schema changes) to avoid the PostgreSQL error "cannot
ALTER TABLE because it has pending trigger events."
- Fix backfill logic to prefer `OBJECT` over `VIEW` for navigation menu
items that have `targetObjectMetadataId`, and correct already mis-typed
items. Tighten the `CHECK` constraint to enforce `viewId IS NULL` for
`OBJECT` type items.
- On the frontend, force `navigationMenuItems` into `staleEntityKeys`
when the server's `minimalMetadata` response omits the collection hash
(happens when the Redis cache hasn't been warmed after an upgrade),
ensuring the sidebar loads navigation items.

## Test plan

- [ ] Upgrade from 1.18 or 1.19 to 1.20 and verify the migration
completes without errors
- [ ] Verify navigation menu items of type `OBJECT` do not have a
`viewId` set in the database
- [ ] Sign out and sign in — confirm navigation menu items appear in the
sidebar on first load
- [ ] Verify `VIEW`-typed items also appear correctly in the sidebar

Made with [Cursor](https://cursor.com)
2026-03-18 11:56:19 +01:00
neo773andGitHub 87efaf2ff8 workspace:export command (#18695)
This PR adds a `workspace:export` server command for ongoing debug
tooling/administrative work

Demo Video shows
1. Exporting a sample workspace YC
2. Restoring Local DB Docker volume snapshot to Dropped YC Workspace
3. Importing exported SQL
4. Booting and navigating the imported Workspace



https://github.com/user-attachments/assets/0e1ac6cb-8ce1-440b-8b56-f81dcb27a9c8
2026-03-18 10:40:12 +00:00
Raphaël BosiandGitHub dedcf4e9b9 Support template variables in command menu item labels (#18707)
- Adds template variable interpolation (`${...}`) to command menu item
labels and short labels, enabling dynamic text like `Create new
${capitalize(objectMetadataItem.labelSingular)}` instead of static
`Create new record`.
- Supports `capitalize` and `lowercase` transform functions within
template expressions.
2026-03-18 10:26:58 +00:00
neo773andGitHub 928194cee4 fix Draft Email stuck Callout (#18719) 2026-03-18 10:19:33 +00:00
martmullandGitHub 13a357ab9f Publish new version (#18727)
as title
2026-03-18 09:54:54 +00:00
neo773andGitHub b1a7c5c4d6 Fix null connectedAccount crash in blocklist message deletion job (#18723)
Fixes TWENTY-SERVER-DRP
2026-03-18 10:41:56 +01:00
neo773andGitHub a8c445a1c2 Treat Microsoft Graph 400 with empty body as transient error (#18726)
Graph SDK occasionally returns a 400 with a null error message which is
not a real bad request but a transient hiccup.
Classify these as temporary errors so they get retried instead of
flooding Sentry.

Fixes TWENTY-SERVER-D3X
2026-03-18 10:41:10 +01:00
Charles BochetandGitHub a74edbf715 fix: route object color through standardOverrides for standard objects (#18717)
## Summary

- Fixes object `color` to use the `standardOverrides` mechanism for
standard objects, matching how `label`, `description`, and `icon`
already work
- Previously, color was written directly to the `objectMetadata.color`
column for **both** standard and custom objects, which meant user color
customizations on standard objects could be overwritten during metadata
syncs
- Custom objects continue to have `color` updated directly on the entity
(no change)

## Changes

| File | What changed |
|------|-------------|
| `object-metadata-standard-overrides-properties.constant.ts` | Added
`'color'` to `OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES` so
`sanitizeRawUpdateObjectInput` routes color into `standardOverrides` for
standard objects |
| `resolve-object-metadata-standard-override.util.ts` | Extended to
support `'color'` as a key — handled like `icon` (no i18n/translation,
just direct override check) |
| `object-metadata.resolver.ts` | Added `@ResolveField` for `color` that
resolves through `resolveObjectMetadataStandardOverride`, matching the
existing `labelSingular`/`labelPlural`/`description`/`icon` resolve
fields |
| `flat-object-metadata-validator.service.ts` | Removed `'color'` from
`allowedOverrideKeys` for system objects since it now flows through
`standardOverrides` |
| `resolve-object-metadata-standard-override.util.spec.ts` | Added test
cases for custom object color, standard object color override, and
standard object color fallback |
| `successful-update-one-standard-object-metadata.integration-spec.ts` |
Added `'when updating color'` test case, included `color` in GraphQL
queries and `standardOverrides` fragment, reset color in `afterEach`
cleanup |

## How it works now

| Object type | Color update flow |
|---|---|
| **Custom** | Written directly to `objectMetadata.color` column |
| **Standard** | Stored in `objectMetadata.standardOverrides.color`,
resolved via `@ResolveField` at query time |

This is identical to how `label`, `description`, and `icon` have always
worked.

## Test plan

- [x] Unit tests pass
(`resolve-object-metadata-standard-override.util.spec.ts` — 21 tests)
- [x] Typecheck passes (`npx nx typecheck twenty-server`)
- [x] Lint passes (`npx nx lint:diff-with-main twenty-server`)
- [ ] Integration test snapshot regenerates correctly
(`successful-update-one-standard-object-metadata`)
- [ ] Verify standard object color editing from sidebar persists via
`standardOverrides`
- [ ] Verify custom object color editing from sidebar persists directly
on entity

Made with [Cursor](https://cursor.com)
2026-03-18 10:40:57 +01:00
Charles BochetandGitHub 0ae62898a3 fix: restructure navigationMenuItem type migration for safe upgrade path (#18722)
## Summary

- Restructures the `navigationMenuItem.type` column migration to follow
the established 3-step safe upgrade pattern (used for webhooks, files,
etc.)
- The previous migration added `type` as `NOT NULL DEFAULT 'VIEW'` with
a CHECK constraint in one step, which incorrectly assigned `VIEW` to all
existing rows regardless of their actual type (folders, records, links,
objects)
- Now: (1) migration adds column as nullable, (2) upgrade command
backfills correct type from existing columns (`viewId`,
`targetRecordId`, `targetObjectMetadataId`, `link`) and cleans
conflicting columns, (3) shared utility applies `NOT NULL` + `CHECK`
constraint

### Changes

**Modified:**
- `1773681736596-add-type-to-navigation-menu-item.ts` -- adds column as
nullable, no DEFAULT, no CHECK
- `navigation-menu-item.entity.ts` -- removed `default:
NavigationMenuItemType.VIEW` from column decorator
- `upgrade.command.ts` / `1-19-upgrade-version-command.module.ts` --
wired new command

**Created:**
- `1773681736596-makeNavigationMenuItemTypeNotNull.util.ts` -- shared
utility applying NOT NULL + CHECK constraint
- `1773822077682-make-navigation-menu-item-type-not-null.ts` --
migration calling the util with savepoint (succeeds on fresh installs,
swallows error on upgrades with NULL data)
- `1-19-backfill-navigation-menu-item-type.command.ts` -- upgrade
command that backfills type, cleans conflicting columns, then applies
constraints via the shared utility

### Execution flow

**Existing deployments (upgrade):**
1. TypeORM migration adds nullable `type` column, drops old CHECK
2. Savepoint migration fails gracefully (existing rows have NULL type)
3. 1-18 favorites migration creates items WITH correct type
4. 1-19 backfill command infers type for remaining NULL rows, cleans
conflicting columns, applies NOT NULL + CHECK

**Fresh installs:**
1. TypeORM migration adds nullable `type` column
2. Savepoint migration succeeds immediately (no data, constraints apply
cleanly)

## Test plan

- [x] `npx nx typecheck twenty-server` passes
- [x] `npx nx lint:diff-with-main twenty-server` passes
- [x] `npx nx test twenty-server` passes (477 suites, 4297 tests)
- [ ] Verify fresh database setup with `npx nx database:reset
twenty-server` applies both migrations and constraints correctly
- [ ] Verify upgrade path: existing navigation menu items get correct
type backfilled based on their columns

Made with [Cursor](https://cursor.com)
2026-03-18 10:39:50 +01:00
neo773andGitHub fb5b68f1d8 Skip threads with no participants in timeline formatting (#18725)
Threads with no FROM participants have no entry in the participants map,
causing extractParticipantSummary to receive undefined and crash

Fixes TWENTY-SERVER-FB8
2026-03-18 10:32:49 +01:00
2092 changed files with 85078 additions and 24974 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(git stash:*)"
]
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
.git
.env
node_modules
**/node_modules
.nx/cache
packages/twenty-server/.env
+68
View File
@@ -0,0 +1,68 @@
name: AI Catalog Sync
on:
schedule:
- cron: '0 6 * * *' # Daily at 6 AM UTC
workflow_dispatch: # Allow manual trigger
permissions:
contents: write
pull-requests: write
jobs:
sync-catalog:
runs-on: ubuntu-latest
timeout-minutes: 10
env:
NODE_OPTIONS: '--max-old-space-size=4096'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: main
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build dependencies
run: npx nx build twenty-shared
- name: Run catalog sync
run: npx nx run twenty-server:ts-node-no-deps-transpile-only -- ./scripts/ai-sync-models-dev.ts
- name: Check for changes
id: changes
run: |
if git diff --quiet packages/twenty-server/src/engine/metadata-modules/ai/ai-models/ai-providers.json; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- name: Create pull request
if: steps.changes.outputs.changed == 'true'
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'chore: sync AI model catalog from models.dev'
title: 'chore: sync AI model catalog from models.dev'
body: |
Automated daily sync of `ai-providers.json` from [models.dev](https://models.dev).
This PR updates pricing, context windows, and model availability based on the latest data.
New models meeting inclusion criteria (tool calling, pricing data, context limits) are added automatically.
Deprecated models are detected based on cost-efficiency within the same model family.
**Please review before merging** — verify no critical models were incorrectly deprecated.
branch: chore/ai-catalog-sync
base: main
labels: ai, automated
delete-branch: true
- name: Trigger automerge
if: steps.changes.outputs.changed == 'true'
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
repository: twentyhq/twenty-infra
event-type: automated-pr-ready
+2 -2
View File
@@ -10,8 +10,8 @@ permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'merge_group' && github.event.merge_group.base_ref || github.ref }}
cancel-in-progress: ${{ github.event_name != 'merge_group' }}
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
e2e-test:
+58 -5
View File
@@ -1,4 +1,4 @@
name: CI Docker Compose
name: CI Docker
permissions:
contents: read
@@ -19,7 +19,7 @@ jobs:
files: |
packages/twenty-docker/**
docker-compose.yml
test:
test-compose:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
@@ -30,10 +30,10 @@ jobs:
- name: Run compose
run: |
echo "Patching docker-compose.yml..."
# change image to localbuild using yq
yq eval 'del(.services.server.image)' -i docker-compose.yml
yq eval '.services.server.build.context = "../../"' -i docker-compose.yml
yq eval '.services.server.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i docker-compose.yml
yq eval '.services.server.build.target = "twenty"' -i docker-compose.yml
yq eval '.services.server.restart = "no"' -i docker-compose.yml
echo "Setting up .env file..."
@@ -89,11 +89,64 @@ jobs:
echo "Still waiting for server... (${count}/300s)"
done
working-directory: ./packages/twenty-docker/
ci-test-docker-compose-status-check:
test-app-dev:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Create frontend placeholder
run: |
mkdir -p packages/twenty-front/build
echo '<html><body>CI placeholder</body></html>' > packages/twenty-front/build/index.html
- name: Build app-dev image
run: |
docker build \
--target twenty-app-dev \
-f packages/twenty-docker/twenty/Dockerfile \
-t twenty-app-dev-ci \
.
- name: Start container
run: |
docker run -d --name twenty-app-dev \
-p 3000:3000 \
twenty-app-dev-ci
docker logs twenty-app-dev -f &
- name: Wait for server health
run: |
echo "Waiting for twenty-app-dev to become healthy..."
count=0
while true; do
status=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/healthz 2>/dev/null || echo "000")
if [ "$status" = "200" ]; then
echo "Server is healthy!"
curl -s http://localhost:3000/healthz
break
fi
container_status=$(docker inspect --format='{{.State.Status}}' twenty-app-dev 2>/dev/null || echo "unknown")
if [ "$container_status" = "exited" ]; then
echo "Container exited unexpectedly"
docker logs twenty-app-dev
exit 1
fi
count=$((count+1))
if [ $count -gt 300 ]; then
echo "Server did not become healthy within 5 minutes"
docker logs twenty-app-dev
exit 1
fi
echo "Still waiting... (${count}/300s) [HTTP ${status}]"
sleep 1
done
ci-test-docker-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, test]
needs: [changed-files-check, test-compose, test-app-dev]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
+112
View File
@@ -0,0 +1,112 @@
name: CI UI
on:
pull_request:
merge_group:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
if: github.event_name != 'merge_group'
uses: ./.github/workflows/changed-files.yaml
with:
files: |
package.json
yarn.lock
packages/twenty-ui/**
packages/twenty-shared/**
ui-task:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
strategy:
matrix:
task: [lint, typecheck, test]
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Run ${{ matrix.task }}
run: npx nx ${{ matrix.task }} twenty-ui
ui-sb-build:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build storybook
run: npx nx storybook:build twenty-ui
- name: Upload storybook build
uses: actions/upload-artifact@v4
with:
name: storybook-twenty-ui
path: packages/twenty-ui/storybook-static
retention-days: 1
ui-sb-test:
timeout-minutes: 30
runs-on: ubuntu-latest
needs: ui-sb-build
env:
STORYBOOK_URL: http://localhost:6007
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build dependencies
run: npx nx build twenty-shared
- name: Download storybook build
uses: actions/download-artifact@v4
with:
name: storybook-twenty-ui
path: packages/twenty-ui/storybook-static
- name: Install Playwright
run: |
cd packages/twenty-ui
npx playwright install
- name: Serve storybook & run tests
run: |
npx http-server packages/twenty-ui/storybook-static --port 6007 --silent &
timeout 30 bash -c 'until curl -sf http://localhost:6007 > /dev/null 2>&1; do sleep 1; done'
npx nx storybook:test twenty-ui
ci-ui-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs:
[
changed-files-check,
ui-task,
ui-sb-build,
ui-sb-test,
]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1
+8
View File
@@ -150,3 +150,11 @@ jobs:
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger i18n automerge
if: github.event_name != 'pull_request' && steps.check_changes.outputs.changes_detected == 'true'
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
repository: twentyhq/twenty-infra
event-type: i18n-pr-ready
+8
View File
@@ -138,3 +138,11 @@ jobs:
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger i18n automerge
if: steps.compile_translations.outputs.changes_detected == 'true'
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
repository: twentyhq/twenty-infra
event-type: i18n-pr-ready
+8
View File
@@ -102,3 +102,11 @@ jobs:
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger i18n automerge
if: steps.check_extract_changes.outputs.changes_detected == 'true' || steps.check_compile_changes.outputs.changes_detected == 'true'
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
repository: twentyhq/twenty-infra
event-type: i18n-pr-ready
@@ -24,10 +24,12 @@ jobs:
yq eval 'del(.services.server.image)' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.server.build.context = "../../"' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.server.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.server.build.target = "twenty"' -i packages/twenty-docker/docker-compose.yml
yq eval 'del(.services.worker.image)' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.worker.build.context = "../../"' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.worker.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.worker.build.target = "twenty"' -i packages/twenty-docker/docker-compose.yml
echo "Adding SIGN_IN_PREFILLED environment variable to server service..."
yq eval '.services.server.environment.SIGN_IN_PREFILLED = "${SIGN_IN_PREFILLED}"' -i packages/twenty-docker/docker-compose.yml
@@ -0,0 +1,144 @@
name: Visual Regression Dispatch
# Uses workflow_run to dispatch visual regression to ci-privileged.
# This runs in the context of the base repo (not the fork), so it has
# access to secrets — making it work for external contributor PRs.
on:
workflow_run:
workflows: ['CI Front', 'CI UI']
types: [completed]
permissions:
actions: read
pull-requests: read
jobs:
dispatch:
if: >-
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Determine project and artifact name
id: project
uses: actions/github-script@v7
with:
script: |
const workflowName = context.payload.workflow_run.name;
if (workflowName === 'CI Front') {
core.setOutput('project', 'twenty-front');
core.setOutput('artifact_name', 'storybook-static');
core.setOutput('tarball_name', 'storybook-twenty-front-tarball');
core.setOutput('tarball_file', 'storybook-twenty-front.tar.gz');
} else if (workflowName === 'CI UI') {
core.setOutput('project', 'twenty-ui');
core.setOutput('artifact_name', 'storybook-twenty-ui');
core.setOutput('tarball_name', 'storybook-twenty-ui-tarball');
core.setOutput('tarball_file', 'storybook-twenty-ui.tar.gz');
} else {
core.setFailed(`Unexpected workflow: ${workflowName}`);
}
- name: Check if storybook artifact exists
id: check-artifact
uses: actions/github-script@v7
with:
script: |
const artifactName = '${{ steps.project.outputs.artifact_name }}';
const runId = context.payload.workflow_run.id;
const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: runId,
});
const found = artifacts.artifacts.some(a => a.name === artifactName);
core.setOutput('exists', found ? 'true' : 'false');
if (!found) {
core.info(`Artifact "${artifactName}" not found in run ${runId} — storybook build was likely skipped`);
}
- name: Get PR number
if: steps.check-artifact.outputs.exists == 'true'
id: pr-info
uses: actions/github-script@v7
with:
script: |
const headBranch = context.payload.workflow_run.head_branch;
const headRepo = context.payload.workflow_run.head_repository;
// workflow_run.pull_requests is empty for fork PRs,
// so fall back to searching by head label (owner:branch)
let pullRequests = context.payload.workflow_run.pull_requests;
let prNumber;
if (pullRequests && pullRequests.length > 0) {
prNumber = pullRequests[0].number;
} else {
const headLabel = `${headRepo.owner.login}:${headBranch}`;
core.info(`pull_requests is empty (likely a fork PR), searching by head label: ${headLabel}`);
const { data: prs } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
head: headLabel,
per_page: 1,
});
if (prs.length > 0) {
prNumber = prs[0].number;
}
}
if (!prNumber) {
core.info('No pull request found for this workflow run — skipping');
core.setOutput('has_pr', 'false');
return;
}
core.setOutput('pr_number', prNumber);
core.setOutput('has_pr', 'true');
core.info(`PR #${prNumber}`);
- name: Download storybook artifact from triggering run
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
uses: actions/download-artifact@v4
with:
name: ${{ steps.project.outputs.artifact_name }}
path: storybook-static
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
- name: Package storybook
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
run: tar -czf /tmp/${{ steps.project.outputs.tarball_file }} -C storybook-static .
- name: Upload storybook tarball
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
uses: actions/upload-artifact@v4
with:
name: ${{ steps.project.outputs.tarball_name }}
path: /tmp/${{ steps.project.outputs.tarball_file }}
retention-days: 1
- name: Dispatch to ci-privileged
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
repository: twentyhq/ci-privileged
event-type: visual-regression
client-payload: >-
{
"pr_number": "${{ steps.pr-info.outputs.pr_number }}",
"run_id": "${{ github.run_id }}",
"repo": "${{ github.repository }}",
"project": "${{ steps.project.outputs.project }}",
"branch": "${{ github.event.workflow_run.head_branch }}",
"commit": "${{ github.event.workflow_run.head_sha }}"
}
+2 -2
View File
@@ -2,8 +2,8 @@
"mcpServers": {
"postgres": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-postgres", "${PG_DATABASE_URL}"],
"command": "bash",
"args": ["-c", "source packages/twenty-server/.env && npx -y @modelcontextprotocol/server-postgres \"$PG_DATABASE_URL\""],
"env": {}
},
"playwright": {
+11
View File
@@ -80,6 +80,17 @@ npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/mi
npx nx run twenty-server:command workspace:sync-metadata
```
### Database Inspection (Postgres MCP)
A read-only Postgres MCP server is configured in `.mcp.json`. Use it to:
- Inspect workspace data, metadata, and object definitions while developing
- Verify migration results (columns, types, constraints) after running migrations
- Explore the multi-tenant schema structure (core, metadata, workspace-specific schemas)
- Debug issues by querying raw data to confirm whether a bug is frontend, backend, or data-level
- Inspect metadata tables to debug GraphQL schema generation or `workspace:sync-metadata` issues
This server is read-only — for write operations (reset, migrations, sync), use the CLI commands above.
### GraphQL
```bash
# Generate GraphQL types (run after schema changes)
+35 -12
View File
@@ -25,25 +25,25 @@ See Twenty application documentation https://docs.twenty.com/developers/extend/c
## Prerequisites
- Node.js 24+ (recommended) and Yarn 4
- A Twenty workspace and an API key (create one at https://app.twenty.com/settings/api-webhooks)
- Docker (for the local Twenty dev server)
## Quick start
```bash
# Scaffold a new app — the CLI will offer to start a local Twenty server
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Get help and list all available commands
yarn twenty help
# The scaffolder can automatically:
# 1. Start a local Twenty server (Docker)
# 2. Open the browser to log in (tim@apple.dev / tim@apple.dev)
# 3. Authenticate your app via OAuth
# Authenticate with your Twenty server
yarn twenty remote add --local
# Add a new entity to your application (guided)
yarn twenty add
# Or do it manually:
yarn twenty server start # Start local Twenty server
yarn twenty remote add --local # Authenticate via OAuth
# Start dev mode: watches, builds, and syncs local changes to your workspace
# (also auto-generates typed CoreApiClient — MetadataApiClient ships pre-built with the SDK — both available via `twenty-sdk/clients`)
yarn twenty dev
# Watch your application's function logs
@@ -107,10 +107,32 @@ npx create-twenty-app@latest my-app -m
- `skills/example-skill.ts` — Example AI agent skill definition
- `__tests__/app-install.integration-test.ts` — Integration test that builds, installs, and verifies the app (includes `vitest.config.ts`, `tsconfig.spec.json`, and a setup file)
## Local server
The scaffolder can start a local Twenty dev server for you (all-in-one Docker image with PostgreSQL, Redis, server, and worker). You can also manage it manually:
```bash
yarn twenty server start # Start (pulls image if needed)
yarn twenty server status # Check if it's healthy
yarn twenty server logs # Stream logs
yarn twenty server stop # Stop (data is preserved)
yarn twenty server reset # Wipe all data and start fresh
```
The server is pre-seeded with a workspace and user (`tim@apple.dev` / `tim@apple.dev`).
### How to use a local Twenty instance
If you're already running a local Twenty instance, you can connect to it instead of using Docker. Pass the port your local server is listening on (default: `3000`):
```bash
npx create-twenty-app@latest my-app --port 3000
```
## Next steps
- Run `yarn twenty help` to see all available commands.
- Use `yarn twenty remote add --local` to authenticate with your Twenty workspace.
- Use `yarn twenty remote add --local` to authenticate with your Twenty workspace via OAuth.
- Explore the generated project and add your first entity with `yarn twenty add` (logic functions, front components, objects, roles, views, navigation menu items, skills).
- Use `yarn twenty dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
- `CoreApiClient` (for workspace data via `/graphql`) is auto-generated by `yarn twenty dev`. `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`) ships pre-built with the SDK. Both are available via `import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/clients'`.
@@ -153,8 +175,9 @@ Our team reviews contributions for quality, security, and reusability before mer
## Troubleshooting
- Auth prompts not appearing: run `yarn twenty remote add --local` again and verify the API key permissions.
- Types not generated: ensure `yarn twenty dev` is running — it autogenerates the typed client.
- Server not starting: check Docker is running (`docker info`), then try `yarn twenty server logs`.
- Auth not working: make sure you're logged in to Twenty in the browser first, then run `yarn twenty remote add --local`.
- Types not generated: ensure `yarn twenty dev` is running — it auto-generates the typed client.
## Contributing
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "create-twenty-app",
"version": "0.7.0",
"version": "0.8.0-canary.2",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",
+8
View File
@@ -31,6 +31,10 @@ const program = new Command(packageJson.name)
'--skip-local-instance',
'Skip the local Twenty instance setup prompt',
)
.option(
'-p, --port <port>',
'Port of an existing Twenty server (skips Docker setup)',
)
.helpOption('-h, --help', 'Display this help message.')
.action(
async (
@@ -42,6 +46,7 @@ const program = new Command(packageJson.name)
displayName?: string;
description?: string;
skipLocalInstance?: boolean;
port?: string;
},
) => {
const modeFlags = [options?.exhaustive, options?.minimal].filter(Boolean);
@@ -71,6 +76,8 @@ const program = new Command(packageJson.name)
const mode: ScaffoldingMode = options?.minimal ? 'minimal' : 'exhaustive';
const port = options?.port ? parseInt(options.port, 10) : undefined;
await new CreateAppCommand().execute({
directory,
mode,
@@ -78,6 +85,7 @@ const program = new Command(packageJson.name)
displayName: options?.displayName,
description: options?.description,
skipLocalInstance: options?.skipLocalInstance,
port,
});
},
);
@@ -1,60 +1,11 @@
This is a [Twenty](https://twenty.com) application project bootstrapped with [`create-twenty-app`](https://www.npmjs.com/package/create-twenty-app).
This is a [Twenty](https://twenty.com) application bootstrapped with [`create-twenty-app`](https://www.npmjs.com/package/create-twenty-app).
## Getting Started
Start development mode — it auto-connects to your local Twenty server at localhost:3000:
```bash
yarn twenty dev
```
Open your Twenty instance and go to `/settings/applications` section to see the result.
## Available Commands
Run `yarn twenty help` to list all available commands. Common commands:
```bash
# Remotes & authentication
yarn twenty remote add --local # Connect to local Twenty server
yarn twenty remote add <url> # Connect to a remote server (OAuth)
yarn twenty remote list # List all configured remotes
yarn twenty remote switch # Switch default remote
yarn twenty remote status # Check auth status
yarn twenty remote remove <name> # Remove a remote
# Development
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
yarn twenty build # Build the application
yarn twenty deploy # Deploy to a Twenty server
yarn twenty publish # Publish to npm
yarn twenty add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
yarn twenty exec # Execute a function with JSON payload
yarn twenty logs # Stream function logs
yarn twenty uninstall # Uninstall app from server
```
## Integration Tests
If your project includes the example integration test (`src/__tests__/app-install.integration-test.ts`), you can run it with:
```bash
# Make sure a Twenty server is running at http://localhost:3000
yarn test
```
The test builds and installs the app, then verifies it appears in the applications list. Test configuration (API URL and API key) is defined in `vitest.config.ts`.
## LLMs instructions
Main docs and pitfalls are available in LLMS.md file.
Run `yarn twenty help` to list all available commands.
## Learn More
To learn more about Twenty applications, take a look at the following resources:
- [twenty-sdk](https://www.npmjs.com/package/twenty-sdk) - learn about `twenty-sdk` tool.
- [Twenty doc](https://docs.twenty.com/) - Twenty's documentation.
- Join our [Discord](https://discord.gg/cx5n4Jzs57)
You can check out [the Twenty GitHub repository](https://github.com/twentyhq/twenty) - your feedback and contributions are welcome!
- [Twenty Apps documentation](https://docs.twenty.com/developers/extend/capabilities/apps)
- [twenty-sdk CLI reference](https://www.npmjs.com/package/twenty-sdk)
- [Discord](https://discord.gg/cx5n4Jzs57)
@@ -1,3 +1,4 @@
import { basename } from 'path';
import { copyBaseApplicationProject } from '@/utils/app-template';
import { convertToLabel } from '@/utils/convert-to-label';
import { install } from '@/utils/install';
@@ -28,14 +29,15 @@ type CreateAppOptions = {
displayName?: string;
description?: string;
skipLocalInstance?: boolean;
port?: number;
};
export class CreateAppCommand {
async execute(options: CreateAppOptions = {}): Promise<void> {
try {
const { appName, appDisplayName, appDirectory, appDescription } =
await this.getAppInfos(options);
const { appName, appDisplayName, appDirectory, appDescription } =
await this.getAppInfos(options);
try {
const exampleOptions = this.resolveExampleOptions(
options.mode ?? 'exhaustive',
);
@@ -61,29 +63,17 @@ export class CreateAppCommand {
let localResult: LocalInstanceResult = { running: false };
if (!options.skipLocalInstance) {
const { needsLocalInstance } = await inquirer.prompt([
{
type: 'confirm',
name: 'needsLocalInstance',
message:
'Do you need a local instance of Twenty? Recommended if you not have one already.',
default: true,
},
]);
localResult = await setupLocalInstance(appDirectory, options.port);
if (needsLocalInstance) {
localResult = await setupLocalInstance();
}
if (isDefined(localResult.apiKey)) {
this.runAuthLogin(appDirectory, localResult.apiKey);
if (localResult.running && localResult.serverUrl) {
await this.connectToLocal(appDirectory, localResult.serverUrl);
}
}
this.logSuccess(appDirectory, localResult);
} catch (error) {
console.error(
chalk.red('Initialization failed:'),
chalk.red('\nCreate application failed:'),
error instanceof Error ? error.message : error,
);
process.exit(1);
@@ -201,23 +191,25 @@ export class CreateAppCommand {
appDirectory: string;
appName: string;
}): void {
console.log(chalk.blue('🎯 Creating Twenty Application'));
console.log(chalk.gray(`📁 Directory: ${appDirectory}`));
console.log(chalk.gray(`📝 Name: ${appName}`));
console.log('');
console.log(
chalk.blue('\n', 'Creating Twenty Application\n'),
chalk.gray(`- Directory: ${appDirectory}\n`, `- Name: ${appName}\n`),
);
}
private runAuthLogin(appDirectory: string, apiKey: string): void {
private async connectToLocal(
appDirectory: string,
serverUrl: string,
): Promise<void> {
try {
execSync(
`yarn twenty auth:login --api-key "${apiKey}" --api-url http://localhost:3000`,
{ cwd: appDirectory, stdio: 'inherit' },
);
console.log(chalk.green('✅ Authenticated with local Twenty instance.'));
execSync(`yarn twenty remote add ${serverUrl} --as local`, {
cwd: appDirectory,
stdio: 'inherit',
});
} catch {
console.log(
chalk.yellow(
'⚠️ Auto auth:login failed. Run `yarn twenty auth:login` manually.',
'Authentication skipped. Run `yarn twenty remote add --local` manually.',
),
);
}
@@ -227,29 +219,21 @@ export class CreateAppCommand {
appDirectory: string,
localResult: LocalInstanceResult,
): void {
const dirName = appDirectory.split('/').reverse()[0] ?? '';
const dirName = basename(appDirectory);
console.log(chalk.green('Application created!'));
console.log('');
console.log(chalk.blue('Next steps:'));
console.log(chalk.gray(` cd ${dirName}`));
console.log(chalk.blue('\nApplication created. Next steps:'));
console.log(chalk.gray(`- cd ${dirName}`));
if (localResult.apiKey) {
console.log(chalk.gray(' yarn twenty app:dev # Start dev mode'));
} else if (localResult.running) {
if (!localResult.running) {
console.log(
chalk.gray(
' yarn twenty remote add --local # Authenticate with Twenty',
'- yarn twenty remote add --local # Authenticate with Twenty',
),
);
console.log(chalk.gray(' yarn twenty app:dev # Start dev mode'));
} else {
console.log(
chalk.gray(
' yarn twenty remote add --local # Authenticate with Twenty',
),
);
console.log(chalk.gray(' yarn twenty app:dev # Start dev mode'));
}
console.log(
chalk.gray('- yarn twenty dev # Start dev mode'),
);
}
}
@@ -5,6 +5,7 @@ import { exec } from 'child_process';
const execPromise = promisify(exec);
export const install = async (root: string) => {
console.log(chalk.gray('Installing yarn dependencies...'));
try {
await execPromise('corepack enable', { cwd: root });
} catch (error: any) {
@@ -14,6 +15,6 @@ export const install = async (root: string) => {
try {
await execPromise('yarn install', { cwd: root });
} catch (error: any) {
console.error(chalk.red('yarn install failed:'), error.stdout);
console.warn(chalk.yellow('yarn install failed:'), error.stdout);
}
};
@@ -1,194 +1,106 @@
import chalk from 'chalk';
import inquirer from 'inquirer';
import { execSync } from 'node:child_process';
import { isDefined } from 'twenty-shared/utils';
const INSTALL_SCRIPT_URL =
'https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/scripts/install.sh';
const LOCAL_PORTS = [2020, 3000];
const SERVER_CONTAINER = 'twenty-server-1';
const DB_CONTAINER = 'twenty-db-1';
// Minimal health check — the full implementation lives in twenty-sdk
const isServerReady = async (port: number): Promise<boolean> => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000);
const isDockerAvailable = (): boolean => {
try {
execSync('docker compose version', { stdio: 'ignore' });
return true;
} catch {
return false;
}
};
const isDockerRunning = (): boolean => {
try {
execSync('docker info', { stdio: 'ignore' });
return true;
} catch {
return false;
}
};
const isTwentyServerRunning = async (): Promise<boolean> => {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000);
const response = await fetch('http://localhost:3000/healthz', {
const response = await fetch(`http://localhost:${port}/healthz`, {
signal: controller.signal,
});
clearTimeout(timeoutId);
const body = await response.json();
return body.status === 'ok';
} catch {
return false;
} finally {
clearTimeout(timeoutId);
}
};
const getActiveWorkspaceId = (): string | null => {
try {
const result = execSync(
`docker exec ${DB_CONTAINER} psql -U postgres -d default -t -c "SELECT id FROM core.workspace WHERE \\"activationStatus\\" = 'ACTIVE' LIMIT 1"`,
{ encoding: 'utf-8' },
).trim();
const detectRunningServer = async (
preferredPort?: number,
): Promise<number | null> => {
const ports = preferredPort ? [preferredPort] : LOCAL_PORTS;
return result || null;
} catch {
return null;
}
};
const generateApiKeyToken = (workspaceId: string): string | null => {
try {
const output = execSync(
`docker exec -e NODE_ENV=development ${SERVER_CONTAINER} yarn command:prod workspace:generate-api-key -w ${workspaceId}`,
{ encoding: 'utf-8' },
);
const TOKEN_PREFIX = 'TOKEN:';
const tokenLine = output
.trim()
.split('\n')
.find((line) => line.includes(TOKEN_PREFIX));
if (!tokenLine) {
return null;
for (const port of ports) {
if (await isServerReady(port)) {
return port;
}
const tokenStartIndex =
tokenLine.indexOf(TOKEN_PREFIX) + TOKEN_PREFIX.length;
return tokenLine.slice(tokenStartIndex).trim();
} catch {
return null;
}
return null;
};
export type LocalInstanceResult = {
running: boolean;
apiKey?: string;
serverUrl?: string;
};
export const setupLocalInstance = async (): Promise<LocalInstanceResult> => {
console.log('');
console.log(chalk.blue('🐳 Setting up local Twenty instance...'));
export const setupLocalInstance = async (
appDirectory: string,
preferredPort?: number,
): Promise<LocalInstanceResult> => {
const detectedPort = await detectRunningServer(preferredPort);
if (await isTwentyServerRunning()) {
console.log(
chalk.green('✅ Twenty server is already running on localhost:3000.'),
);
} else {
if (!isDockerAvailable()) {
console.log(
chalk.yellow(
'⚠️ Docker Compose is not installed. Please install Docker first.',
),
);
console.log(chalk.gray(' See https://docs.docker.com/get-docker/'));
if (detectedPort) {
const serverUrl = `http://localhost:${detectedPort}`;
return { running: false };
}
console.log(chalk.green(`Twenty server detected on ${serverUrl}.\n`));
if (!isDockerRunning()) {
console.log(
chalk.yellow(
'⚠️ Docker is not running. Please start Docker and try again.',
),
);
return { running: false };
return { running: true, serverUrl };
}
if (preferredPort) {
console.log(
chalk.yellow(
`No Twenty server found on port ${preferredPort}.\n` +
'Start your server and run `yarn twenty remote add --local` manually.\n',
),
);
return { running: false };
}
console.log(chalk.blue('Setting up local Twenty instance...\n'));
try {
execSync('yarn twenty server start', {
cwd: appDirectory,
stdio: 'inherit',
});
} catch {
return { running: false };
}
console.log(chalk.gray('Waiting for Twenty to be ready...\n'));
const startTime = Date.now();
const timeoutMs = 180 * 1000;
while (Date.now() - startTime < timeoutMs) {
if (await isServerReady(LOCAL_PORTS[0])) {
const serverUrl = `http://localhost:${LOCAL_PORTS[0]}`;
console.log(chalk.green(`Server running on '${serverUrl}'\n`));
return { running: true, serverUrl };
}
try {
execSync(`bash <(curl -sL ${INSTALL_SCRIPT_URL})`, {
stdio: 'inherit',
shell: '/bin/bash',
});
} catch {
console.log(
chalk.yellow('⚠️ Local instance setup did not complete successfully.'),
);
return { running: false };
}
await new Promise((resolve) => setTimeout(resolve, 2000));
}
console.log('');
console.log(
chalk.blue(
'👉 Please create your workspace in the browser before continuing.',
chalk.yellow(
'Twenty server did not become healthy in time.\n',
"Check: 'yarn twenty server logs'\n",
),
);
const { workspaceCreated } = await inquirer.prompt([
{
type: 'confirm',
name: 'workspaceCreated',
message: 'Have you finished creating your workspace?',
default: true,
},
]);
if (!workspaceCreated) {
console.log(
chalk.yellow(
'⚠️ Skipping API key generation. Run `yarn twenty remote add --local` manually after creating your workspace.',
),
);
return { running: true };
}
console.log(chalk.blue('🔑 Generating API key for your workspace...'));
const workspaceId = getActiveWorkspaceId();
if (!isDefined(workspaceId)) {
console.log(
chalk.yellow(
'⚠️ No active workspace found. Make sure you completed the signup flow, then run `yarn twenty auth:login` manually.',
),
);
return { running: true };
}
const apiKey = generateApiKeyToken(workspaceId);
if (!isDefined(apiKey)) {
console.log(
chalk.yellow(
'⚠️ Could not generate API key. Run `yarn twenty auth:login` manually.',
),
);
return { running: true };
}
console.log(chalk.green('✅ API key generated for your workspace.'));
return { running: true, apiKey };
return { running: false };
};
@@ -93,7 +93,7 @@ import * as os from 'os';
import * as path from 'path';
import { beforeAll } from 'vitest';
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:3000';
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
const assertServerIsReachable = async () => {
+1 -1
View File
@@ -18,7 +18,7 @@ DOCKER_NETWORK=twenty_network
# =============================================================================
prod-build:
@cd ../.. && docker build -f ./packages/twenty-docker/twenty/Dockerfile --platform $(PLATFORM) --tag twenty:$(TAG) . && cd -
@cd ../.. && docker build --target twenty -f ./packages/twenty-docker/twenty/Dockerfile --platform $(PLATFORM) --tag twenty:$(TAG) . && cd -
prod-run:
@docker run -d -p 3000:3000 --name twenty twenty:$(TAG)
@@ -38,7 +38,6 @@ services:
# EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:-contact@yourdomain.com}
# EMAIL_FROM_NAME: ${EMAIL_FROM_NAME:-"John from YourDomain"}
# EMAIL_SYSTEM_ADDRESS: ${EMAIL_SYSTEM_ADDRESS:-system@yourdomain.com}
# EMAIL_DRIVER: ${EMAIL_DRIVER:-smtp}
# EMAIL_SMTP_HOST: ${EMAIL_SMTP_HOST:-smtp.gmail.com}
# EMAIL_SMTP_PORT: ${EMAIL_SMTP_PORT:-465}
@@ -92,7 +91,6 @@ services:
# EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:-contact@yourdomain.com}
# EMAIL_FROM_NAME: ${EMAIL_FROM_NAME:-"John from YourDomain"}
# EMAIL_SYSTEM_ADDRESS: ${EMAIL_SYSTEM_ADDRESS:-system@yourdomain.com}
# EMAIL_DRIVER: ${EMAIL_DRIVER:-smtp}
# EMAIL_SMTP_HOST: ${EMAIL_SMTP_HOST:-smtp.gmail.com}
# EMAIL_SMTP_PORT: ${EMAIL_SMTP_PORT:-465}
@@ -89,6 +89,15 @@ password
{{- end -}}
{{- end -}}
{{/* Check if using external secret for redis password */}}
{{- define "twenty.redis.useExternalSecret" -}}
{{- if and (not .Values.redisInternal.enabled) .Values.redis.external.secretName .Values.redis.external.passwordKey -}}
true
{{- else -}}
false
{{- end -}}
{{- end -}}
{{/* Compose Redis URL */}}
{{- define "twenty.redisUrl" -}}
{{- if .Values.server.env.REDIS_URL -}}
@@ -99,9 +108,14 @@ password
{{- else -}}
{{- $host := .Values.redis.external.host | default "redis" -}}
{{- $port := .Values.redis.external.port | default 6379 -}}
{{- if or (eq (include "twenty.redis.useExternalSecret" .) "true") (.Values.redis.external.password) -}}
{{- $auth := ":$(REDIS_PASSWORD)@" -}}
{{- printf "redis://%s%s:%v" $auth $host $port -}}
{{- else -}}
{{- printf "redis://%s:%v" $host $port -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{/* Compose Server URL from override, ingress, or service */}}
{{- define "twenty.serverUrl" -}}
@@ -46,12 +46,12 @@ spec:
volumeMounts:
- name: redis-data
mountPath: /data
volumes:
- name: redis-data
{{- if .Values.redisInternal.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ .Values.redisInternal.persistence.existingClaim | default (printf "%s-redis" (include "twenty.fullname" .)) }}
{{- else }}
emptyDir: {}
{{- end }}
volumes:
- name: redis-data
{{- if .Values.redisInternal.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ .Values.redisInternal.persistence.existingClaim | default (printf "%s-redis" (include "twenty.fullname" .)) }}
{{- else }}
emptyDir: {}
{{- end }}
{{- end }}
@@ -83,16 +83,16 @@ spec:
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d postgres -v db="${DBNAME}" -Atc "SELECT 1 FROM pg_database WHERE datname = :'db'" | grep -q 1 || \
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d postgres -v db="${DBNAME}" -c 'CREATE DATABASE :"db";'
echo "Creating app user ${APP_USER} if it doesn't exist..."
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d postgres -v app_user="${APP_USER}" -v app_password="${APP_PASSWORD}" <<'EOSQL'
DO
$do$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = :'app_user') THEN
EXECUTE format('CREATE USER %I WITH PASSWORD %L', :'app_user', :'app_password');
END IF;
END
$do$;
EOSQL
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d postgres -v app_user="${APP_USER}" -v app_password="${APP_PASSWORD}" <<'EOSQL'
DO
$do$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = :'app_user') THEN
EXECUTE format('CREATE USER %I WITH PASSWORD %L', :'app_user', :'app_password');
END IF;
END
$do$;
EOSQL
echo "Creating core schema and granting permissions..."
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d "${DBNAME}" -v app_user="${APP_USER}" -c 'CREATE SCHEMA IF NOT EXISTS core'
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d "${DBNAME}" -v db="${DBNAME}" -v app_user="${APP_USER}" -c 'GRANT ALL PRIVILEGES ON DATABASE :"db" TO :"app_user";'
@@ -106,31 +106,6 @@ spec:
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d "${DBNAME}" -v app_user="${APP_USER}" -c 'ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO :"app_user";'
echo "Database ${DBNAME} is ready."
{{- end }}
- name: run-migrations
{{- $img := include "twenty.server.image" . }}
image: {{ include "twenty.image.repository" $img }}:{{ include "twenty.image.tag" $img }}
imagePullPolicy: {{ include "twenty.image.pullPolicy" $img }}
command:
- sh
- -c
- >-
npx -y typeorm migration:run -d dist/database/typeorm/core/core.datasource
env:
{{- if eq (include "twenty.db.useExternalSecret" .) "true" }}
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "twenty.dbPassword.secretName" . }}
key: {{ include "twenty.dbPassword.secretKey" . }}
- name: PG_DATABASE_URL
value: {{ include "twenty.dbUrl.template" . | quote }}
{{- else }}
- name: PG_DATABASE_URL
valueFrom:
secretKeyRef:
name: {{ include "twenty.dbUrl.secretName" . }}
key: url
{{- end }}
containers:
- name: server
{{- $img := include "twenty.server.image" . }}
@@ -154,6 +129,16 @@ spec:
name: {{ include "twenty.dbUrl.secretName" . }}
key: url
{{- end }}
{{- if eq (include "twenty.redis.useExternalSecret" .) "true" }}
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.redis.external.secretName }}
key: {{ .Values.redis.external.passwordKey }}
{{- else if .Values.redis.external.password }}
- name: REDIS_PASSWORD
value: {{ .Values.redis.external.password | quote }}
{{- end }}
- name: REDIS_URL
value: {{ include "twenty.redisUrl" . | quote }}
- name: SIGN_IN_PREFILLED
@@ -171,7 +156,10 @@ spec:
key: accessToken
{{- $storageEnv := (include "twenty.storageEnv" .) }}
{{- if $storageEnv }}
{{ $storageEnv | nindent 12 }}
{{- $storageEnv | nindent 12 }}
{{- end }}
{{- with .Values.server.extraEnv }}
{{- toYaml . | nindent 12 }}
{{- end }}
ports:
- name: http-tcp
@@ -67,6 +67,16 @@ spec:
name: {{ include "twenty.dbUrl.secretName" . }}
key: url
{{- end }}
{{- if eq (include "twenty.redis.useExternalSecret" .) "true" }}
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.redis.external.secretName }}
key: {{ .Values.redis.external.passwordKey }}
{{- else if .Values.redis.external.password }}
- name: REDIS_PASSWORD
value: {{ .Values.redis.external.password | quote }}
{{- end }}
- name: REDIS_URL
value: {{ include "twenty.redisUrl" . | quote }}
- name: STORAGE_TYPE
@@ -76,9 +86,12 @@ spec:
secretKeyRef:
name: {{ include "twenty.secret.tokens.name" . }}
key: accessToken
{{- with .Values.worker.extraEnv }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- $storageEnv := (include "twenty.storageEnv" .) }}
{{- if $storageEnv }}
{{ $storageEnv | nindent 12 }}
{{- $storageEnv | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.worker.resources | nindent 12 }}
@@ -0,0 +1,20 @@
{{- if and .Values.redisInternal.enabled .Values.redisInternal.persistence.enabled (not .Values.redisInternal.persistence.existingClaim) }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "twenty.fullname" . }}-redis
namespace: {{ include "twenty.namespace" . }}
labels:
app.kubernetes.io/name: {{ include "twenty.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: redis
spec:
accessModes:
{{ toYaml .Values.redisInternal.persistence.accessModes | nindent 4 }}
resources:
requests:
storage: {{ .Values.redisInternal.persistence.size }}
{{- if .Values.redisInternal.persistence.storageClass }}
storageClassName: {{ .Values.redisInternal.persistence.storageClass }}
{{- end }}
{{- end }}
@@ -0,0 +1,58 @@
suite: env mapping
templates:
- templates/deployment-server.yaml
- templates/deployment-worker.yaml
release:
name: my-twenty
namespace: default
tests:
- it: renders server extraEnv with plain value
template: templates/deployment-server.yaml
set:
server.extraEnv:
- name: FEATURE_X_ENABLED
value: "true"
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
name: FEATURE_X_ENABLED
value: "true"
- it: renders server extraEnv with valueFrom
template: templates/deployment-server.yaml
set:
server.extraEnv:
- name: SMTP_PASSWORD
valueFrom:
secretKeyRef:
name: smtp-creds
key: password
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
name: SMTP_PASSWORD
valueFrom:
secretKeyRef:
name: smtp-creds
key: password
- it: renders worker extraEnv with valueFrom
template: templates/deployment-worker.yaml
set:
worker.extraEnv:
- name: CUSTOM_SECRET
valueFrom:
secretKeyRef:
name: my-secret
key: custom
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
name: CUSTOM_SECRET
valueFrom:
secretKeyRef:
name: my-secret
key: custom
@@ -0,0 +1,89 @@
suite: redis external authentication
templates:
- templates/deployment-server.yaml
- templates/deployment-worker.yaml
release:
name: my-twenty
namespace: default
tests:
- it: injects REDIS_PASSWORD from external secret into server
template: templates/deployment-server.yaml
set:
redisInternal.enabled: false
redis.external.host: redis.example.com
redis.external.secretName: redis-creds
redis.external.passwordKey: password
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: redis-creds
key: password
- it: injects REDIS_PASSWORD from external secret into worker
template: templates/deployment-worker.yaml
set:
redisInternal.enabled: false
redis.external.host: redis.example.com
redis.external.secretName: redis-creds
redis.external.passwordKey: password
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: redis-creds
key: password
- it: injects plaintext REDIS_PASSWORD when password set directly in server
template: templates/deployment-server.yaml
set:
redisInternal.enabled: false
redis.external.host: redis.example.com
redis.external.password: "s3cr3t"
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
name: REDIS_PASSWORD
value: "s3cr3t"
- it: injects plaintext REDIS_PASSWORD when password set directly in worker
template: templates/deployment-worker.yaml
set:
redisInternal.enabled: false
redis.external.host: redis.example.com
redis.external.password: "s3cr3t"
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
name: REDIS_PASSWORD
value: "s3cr3t"
- it: does not inject REDIS_PASSWORD into server when using internal redis
template: templates/deployment-server.yaml
set:
redisInternal.enabled: true
asserts:
- notContains:
path: spec.template.spec.containers[0].env
content:
name: REDIS_PASSWORD
any: true
- it: does not inject REDIS_PASSWORD into worker when using internal redis
template: templates/deployment-worker.yaml
set:
redisInternal.enabled: true
asserts:
- notContains:
path: spec.template.spec.containers[0].env
content:
name: REDIS_PASSWORD
any: true
@@ -105,18 +105,3 @@ tests:
path: spec.template.spec.initContainers[?(@.name=="ensure-database-exists")].command[2]
pattern: ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES
# TypeORM Migration Tests
# ======================
# TypeORM migrations are configured to use the core.datasource which targets the
# 'core' schema. This ensures the _typeorm_migrations table and all application
# tables use the dedicated core schema.
- it: migrations run against core datasource
template: templates/deployment-server.yaml
set:
db.enabled: true
asserts:
- matchRegex:
path: spec.template.spec.initContainers[?(@.name=="run-migrations")].command[2]
pattern: core\.datasource
@@ -19,7 +19,7 @@ tests:
- crm.example.com
asserts:
- equal:
path: spec.template.spec.containers[0].env[0].value
path: spec.template.spec.containers[0].env[?(@.name=="SERVER_URL")].value
value: "https://crm.example.com:443"
- it: falls back to service when ingress disabled
set:
@@ -28,7 +28,7 @@ tests:
server.env.SERVER_URL: ""
asserts:
- matchRegex:
path: spec.template.spec.containers[0].env[0].value
path: spec.template.spec.containers[0].env[?(@.name=="SERVER_URL")].value
pattern: ^http://my-twenty-twenty-server\.default\.svc\.cluster\.local:3000$
---
suite: ingress configuration
@@ -66,6 +66,7 @@ tests:
set:
server.ingress.acme: true
asserts:
- equal:
path: metadata.annotations[cert-manager.io/cluster-issuer]
value: letsencrypt-prod
- isSubset:
path: metadata.annotations
content:
cert-manager.io/cluster-issuer: letsencrypt-prod
@@ -61,12 +61,27 @@
"EMAIL_SMTP_NO_TLS": { "type": "boolean" },
"EMAIL_FROM_ADDRESS": { "type": "string" },
"EMAIL_FROM_NAME": { "type": "string" },
"EMAIL_SYSTEM_ADDRESS": { "type": "string" },
"IS_EMAIL_VERIFICATION_REQUIRED": { "type": "boolean" },
"EMAIL_VERIFICATION_TOKEN_EXPIRES_IN": { "type": "string" },
"PASSWORD_RESET_TOKEN_EXPIRES_IN": { "type": "string" }
}
},
"extraEnv": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"value": { "type": "string" },
"valueFrom": { "type": "object" }
},
"required": ["name"],
"oneOf": [
{ "required": ["value"] },
{ "required": ["valueFrom"] }
]
}
},
"service": {
"type": "object",
"properties": {
@@ -142,6 +157,22 @@
"STORAGE_TYPE": { "type": "string" },
"DISABLE_DB_MIGRATIONS": { "type": "string" }
}
},
"extraEnv": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"value": { "type": "string" },
"valueFrom": { "type": "object" }
},
"required": ["name"],
"oneOf": [
{ "required": ["value"] },
{ "required": ["valueFrom"] }
]
}
}
}
},
@@ -52,6 +52,14 @@ server:
SIGN_IN_PREFILLED: "false"
ACCESS_TOKEN_EXPIRES_IN: "7d"
LOGIN_TOKEN_EXPIRES_IN: "1h"
extraEnv: []
# - name: EMAIL_DRIVER
# value: smtp
# - name: SMTP_PASSWORD
# valueFrom:
# secretKeyRef:
# name: smtp-creds
# key: password
service:
type: ClusterIP
@@ -105,6 +113,8 @@ worker:
cpu: 1000m
memory: 2048Mi
extraEnv: []
# PostgreSQL
db:
enabled: true
@@ -174,3 +184,6 @@ redis:
external:
host: ""
port: 6379
password: ""
secretName: ""
passwordKey: ""
@@ -0,0 +1 @@
/bin/sh /etc/s6-overlay/scripts/init-db.sh
@@ -0,0 +1,13 @@
#!/bin/sh
# Initialize PostgreSQL data directory if empty
if [ ! -f /data/postgres/PG_VERSION ]; then
echo "Initializing PostgreSQL data directory..."
su-exec postgres initdb -D /data/postgres --auth=trust --encoding=UTF8
# Allow local connections without password
echo "host all all 127.0.0.1/32 trust" >> /data/postgres/pg_hba.conf
echo "host all all ::1/128 trust" >> /data/postgres/pg_hba.conf
fi
exec su-exec postgres postgres -D /data/postgres \
-c listen_addresses=localhost \
-c unix_socket_directories=/tmp
@@ -0,0 +1,6 @@
#!/bin/sh
exec redis-server \
--dir /data/redis \
--maxmemory-policy noeviction \
--bind 127.0.0.1 \
--protected-mode yes
@@ -0,0 +1,3 @@
#!/bin/sh
cd /app/packages/twenty-server
exec yarn start:prod
@@ -0,0 +1,3 @@
#!/bin/sh
cd /app/packages/twenty-server
exec yarn worker:prod
@@ -0,0 +1,56 @@
#!/bin/sh
set -e
# Wait for PostgreSQL to be ready (timeout after 60s)
echo "Waiting for PostgreSQL..."
TRIES=0
until su-exec postgres pg_isready -h localhost; do
TRIES=$((TRIES + 1))
if [ "$TRIES" -ge 120 ]; then
echo "ERROR: PostgreSQL did not become ready within 60s"
exit 1
fi
sleep 0.5
done
echo "PostgreSQL is ready."
# Create role if it doesn't exist
su-exec postgres psql -h localhost -tc \
"SELECT 1 FROM pg_roles WHERE rolname='twenty'" | grep -q 1 \
|| su-exec postgres psql -h localhost -c "CREATE ROLE twenty WITH LOGIN PASSWORD 'twenty' SUPERUSER"
# Create database if it doesn't exist
su-exec postgres psql -h localhost -tc \
"SELECT 1 FROM pg_database WHERE datname='default'" | grep -q 1 \
|| su-exec postgres createdb -h localhost -O twenty default
# Run Twenty database setup and migrations
cd /app/packages/twenty-server
has_schema=$(PGPASSWORD=twenty psql -h localhost -U twenty -d default -tAc \
"SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'core')")
if [ "$has_schema" = "f" ]; then
echo "Database appears to be empty, running initial setup..."
NODE_OPTIONS="--max-old-space-size=1500" node ./dist/scripts/setup-db.js
fi
# Always run migrations (idempotent — skips already-applied ones)
yarn database:migrate:prod
yarn command:prod cache:flush
yarn command:prod upgrade
yarn command:prod cache:flush
# Only seed on first boot — check if the dev workspace already exists
has_workspace=$(PGPASSWORD=twenty psql -h localhost -U twenty -d default -tAc \
"SELECT EXISTS (SELECT 1 FROM core.workspace WHERE id = '20202020-1c25-4d02-bf25-6aeccf7ea419')")
if [ "$has_workspace" = "f" ]; then
echo "Seeding app dev data..."
yarn command:prod workspace:seed:dev --light || true
else
echo "Dev workspace already seeded, skipping."
fi
echo "Database initialization complete."
+159 -21
View File
@@ -1,9 +1,11 @@
# Base image for common dependencies
# ===========================================================================
# Shared build stages (used by both targets)
# ===========================================================================
FROM node:24-alpine AS common-deps
WORKDIR /app
# Copy only the necessary files for dependency resolution
COPY ./package.json ./yarn.lock ./.yarnrc.yml ./tsconfig.base.json ./nx.json /app/
COPY ./.yarn/releases /app/.yarn/releases
COPY ./.yarn/patches /app/.yarn/patches
@@ -16,25 +18,38 @@ COPY ./packages/twenty-shared/package.json /app/packages/twenty-shared/
COPY ./packages/twenty-front/package.json /app/packages/twenty-front/
COPY ./packages/twenty-sdk/package.json /app/packages/twenty-sdk/
# Install all dependencies
RUN yarn && yarn cache clean && npx nx reset
# Build the back
FROM common-deps AS twenty-server-build
# Copy sourcecode after installing dependences to accelerate subsequents builds
COPY ./packages/twenty-emails /app/packages/twenty-emails
COPY ./packages/twenty-shared /app/packages/twenty-shared
COPY ./packages/twenty-ui /app/packages/twenty-ui
COPY ./packages/twenty-sdk /app/packages/twenty-sdk
COPY ./packages/twenty-server /app/packages/twenty-server
RUN npx nx run twenty-server:lingui:extract && \
npx nx run twenty-server:lingui:compile && \
npx nx run twenty-emails:lingui:extract && \
npx nx run twenty-emails:lingui:compile
RUN npx nx run twenty-server:build
# Bundle setup-db script into a standalone JS file so the final image
# doesn't need tsx or the TypeScript source tree at runtime.
RUN npx esbuild packages/twenty-server/scripts/setup-db.ts \
--bundle --platform=node --outfile=packages/twenty-server/dist/scripts/setup-db.js \
--external:typeorm --external:dotenv --external:pg
# Clean server build output (type declarations and compiled tests are not needed at runtime;
# source maps are kept because twenty-infra extracts them from the image for Sentry uploads)
RUN find /app/packages/twenty-server/dist -name '*.d.ts' -delete \
&& rm -rf /app/packages/twenty-server/dist/packages/twenty-server/test
RUN yarn workspaces focus --production twenty-emails twenty-shared twenty-sdk twenty-server
# Build the front
FROM common-deps AS twenty-front-build
ARG REACT_APP_SERVER_BASE_URL
@@ -43,18 +58,26 @@ COPY ./packages/twenty-front /app/packages/twenty-front
COPY ./packages/twenty-ui /app/packages/twenty-ui
COPY ./packages/twenty-shared /app/packages/twenty-shared
COPY ./packages/twenty-sdk /app/packages/twenty-sdk
RUN npx nx build twenty-front
RUN npx nx run twenty-front:lingui:extract && \
npx nx run twenty-front:lingui:compile
# To skip the memory-intensive frontend build, pre-build on the host:
# npx nx build twenty-front
# The check below will use packages/twenty-front/build/ if it already exists.
RUN if [ -d /app/packages/twenty-front/build ]; then \
echo "Using pre-built frontend from host"; \
else \
NODE_OPTIONS="--max-old-space-size=8192" npx nx build twenty-front; \
fi
# Final stage: Run the application
# ===========================================================================
# Target: twenty (production)
# docker build --target twenty -f packages/twenty-docker/twenty/Dockerfile .
# ===========================================================================
FROM node:24-alpine AS twenty
# Used to run healthcheck in docker
RUN apk add --no-cache curl jq
RUN npm install -g tsx
RUN apk add --no-cache postgresql-client
RUN apk add --no-cache curl jq postgresql-client
COPY ./packages/twenty-docker/twenty/entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
@@ -66,20 +89,135 @@ ENV REACT_APP_SERVER_BASE_URL=$REACT_APP_SERVER_BASE_URL
ARG APP_VERSION
ENV APP_VERSION=$APP_VERSION
# Copy built applications from previous stages
COPY --chown=1000 --from=twenty-server-build /app /app
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-server /app/packages/twenty-server
# Workspace root config
COPY --chown=1000 --from=twenty-server-build /app/package.json /app/yarn.lock /app/.yarnrc.yml /app/
COPY --chown=1000 --from=twenty-server-build /app/tsconfig.base.json /app/nx.json /app/
COPY --chown=1000 --from=twenty-server-build /app/.yarn /app/.yarn
COPY --chown=1000 --from=twenty-server-build /app/node_modules /app/node_modules
# Server package (compiled dist + package.json only, no src/)
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-server/package.json /app/packages/twenty-server/
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-server/dist /app/packages/twenty-server/dist
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-server/patches /app/packages/twenty-server/patches
# Workspace packages (dist + package.json; node_modules symlinks resolve to these)
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-shared/package.json /app/packages/twenty-shared/
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-shared/dist /app/packages/twenty-shared/dist
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-emails/package.json /app/packages/twenty-emails/
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-emails/dist /app/packages/twenty-emails/dist
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-sdk/package.json /app/packages/twenty-sdk/
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-ui/package.json /app/packages/twenty-ui/
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-front/package.json /app/packages/twenty-front/
# Frontend static build
COPY --chown=1000 --from=twenty-front-build /app/packages/twenty-front/build /app/packages/twenty-server/dist/front
# Set metadata and labels
LABEL org.opencontainers.image.source=https://github.com/twentyhq/twenty
LABEL org.opencontainers.image.description="This image provides a consistent and reproducible environment for the backend and frontend, ensuring it deploys faster and runs the same way regardless of the deployment environment."
LABEL org.opencontainers.image.description="Production Twenty image with backend and frontend."
RUN mkdir -p /app/.local-storage /app/packages/twenty-server/.local-storage && \
chown -R 1000:1000 /app
chown 1000:1000 /app/.local-storage /app/packages/twenty-server/.local-storage
# Use non root user with uid 1000
USER 1000
CMD ["node", "dist/main"]
ENTRYPOINT ["/app/entrypoint.sh"]
# ===========================================================================
# Target: twenty-app-dev (all-in-one with Postgres + Redis)
# docker build --target twenty-app-dev -f packages/twenty-docker/twenty/Dockerfile .
# ===========================================================================
FROM alpine:3.20 AS s6-fetch
ARG S6_OVERLAY_VERSION=3.2.0.2
ARG TARGETARCH
RUN if [ "$TARGETARCH" = "arm64" ]; then echo "aarch64" > /tmp/s6arch; \
else echo "x86_64" > /tmp/s6arch; fi
RUN S6_ARCH=$(cat /tmp/s6arch) && \
wget -O /tmp/s6-overlay-noarch.tar.xz \
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz" && \
wget -O /tmp/s6-overlay-noarch.tar.xz.sha256 \
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz.sha256" && \
wget -O /tmp/s6-overlay-arch.tar.xz \
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz" && \
wget -O /tmp/s6-overlay-arch.tar.xz.sha256 \
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz.sha256" && \
cd /tmp && \
NOARCH_SUM=$(awk '{print $1}' s6-overlay-noarch.tar.xz.sha256) && \
ARCH_SUM=$(awk '{print $1}' s6-overlay-arch.tar.xz.sha256) && \
echo "$NOARCH_SUM s6-overlay-noarch.tar.xz" | sha256sum -c - && \
echo "$ARCH_SUM s6-overlay-arch.tar.xz" | sha256sum -c -
FROM node:24-alpine AS twenty-app-dev
# s6-overlay
COPY --from=s6-fetch /tmp/s6-overlay-noarch.tar.xz /tmp/
COPY --from=s6-fetch /tmp/s6-overlay-arch.tar.xz /tmp/
RUN tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz \
&& tar -C / -Jxpf /tmp/s6-overlay-arch.tar.xz \
&& rm /tmp/s6-overlay-*.tar.xz
RUN apk add --no-cache \
postgresql16 postgresql16-contrib \
redis \
curl jq su-exec
# Workspace root config
COPY --from=twenty-server-build /app/package.json /app/yarn.lock /app/.yarnrc.yml /app/
COPY --from=twenty-server-build /app/tsconfig.base.json /app/nx.json /app/
COPY --from=twenty-server-build /app/.yarn /app/.yarn
COPY --from=twenty-server-build /app/node_modules /app/node_modules
# Server package (compiled dist + package.json only, no src/)
COPY --from=twenty-server-build /app/packages/twenty-server/package.json /app/packages/twenty-server/
COPY --from=twenty-server-build /app/packages/twenty-server/dist /app/packages/twenty-server/dist
COPY --from=twenty-server-build /app/packages/twenty-server/patches /app/packages/twenty-server/patches
# Workspace packages (dist + package.json; node_modules symlinks resolve to these)
COPY --from=twenty-server-build /app/packages/twenty-shared/package.json /app/packages/twenty-shared/
COPY --from=twenty-server-build /app/packages/twenty-shared/dist /app/packages/twenty-shared/dist
COPY --from=twenty-server-build /app/packages/twenty-emails/package.json /app/packages/twenty-emails/
COPY --from=twenty-server-build /app/packages/twenty-emails/dist /app/packages/twenty-emails/dist
COPY --from=twenty-server-build /app/packages/twenty-sdk/package.json /app/packages/twenty-sdk/
COPY --from=twenty-server-build /app/packages/twenty-ui/package.json /app/packages/twenty-ui/
COPY --from=twenty-server-build /app/packages/twenty-front/package.json /app/packages/twenty-front/
# Frontend static build
COPY --from=twenty-front-build /app/packages/twenty-front/build /app/packages/twenty-server/dist/front
# Source maps are not needed in the dev image (no Sentry)
RUN find /app/packages/twenty-server/dist -name '*.js.map' -delete
# s6 service definitions
COPY packages/twenty-docker/twenty-app-dev/rootfs/ /
RUN mkdir -p /data/postgres /data/redis /app/.local-storage \
&& chown -R postgres:postgres /data/postgres \
&& chown 1000:1000 /data/redis /app/.local-storage
ARG REACT_APP_SERVER_BASE_URL
ARG APP_VERSION=0.0.0
ENV S6_KEEP_ENV=1
ENV PG_DATABASE_URL=postgres://twenty:twenty@localhost:5432/default \
SERVER_URL=http://localhost:2020 \
REDIS_URL=redis://localhost:6379 \
STORAGE_TYPE=local \
APP_SECRET=twenty-app-dev-secret-not-for-production \
REACT_APP_SERVER_BASE_URL=$REACT_APP_SERVER_BASE_URL \
APP_VERSION=$APP_VERSION \
NODE_ENV=development \
NODE_PORT=3000 \
DISABLE_DB_MIGRATIONS=true \
DISABLE_CRON_JOBS_REGISTRATION=true \
IS_BILLING_ENABLED=false \
SIGN_IN_PREFILLED=true
EXPOSE 3000
VOLUME ["/data/postgres", "/app/.local-storage"]
LABEL org.opencontainers.image.source=https://github.com/twentyhq/twenty
LABEL org.opencontainers.image.description="All-in-one Twenty image for local development and SDK usage. Includes PostgreSQL, Redis, server, and worker."
ENTRYPOINT ["/init"]
+1 -1
View File
@@ -13,7 +13,7 @@ setup_and_migrate_db() {
has_schema=$(psql -tAc "SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'core')" ${PG_DATABASE_URL})
if [ "$has_schema" = "f" ]; then
echo "Database appears to be empty, running migrations."
NODE_OPTIONS="--max-old-space-size=1500" tsx ./scripts/setup-db.ts
NODE_OPTIONS="--max-old-space-size=1500" node ./dist/scripts/setup-db.js
yarn database:migrate:prod
fi
@@ -554,7 +554,7 @@ export default defineFrontComponent({
Key points:
- Front components are React components that render in isolated contexts within Twenty.
- The `component` field references your React component.
- Components are built and synced automatically during `yarn twenty app:dev`.
- Components are built and synced automatically during `yarn twenty dev`.
You can create new front components in two ways:
@@ -597,7 +597,7 @@ You can create new skills in two ways:
### Generated typed clients
Two typed clients are auto-generated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated` based on your workspace schema:
Two typed clients are auto-generated by `yarn twenty dev` and stored in `node_modules/twenty-sdk/generated` based on your workspace schema:
- **`CoreApiClient`** — queries the `/graphql` endpoint for workspace data
- **`MetadataApiClient`** — queries the `/metadata` endpoint for workspace configuration and file uploads
@@ -612,7 +612,7 @@ const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
```
Both clients are re-generated automatically by `yarn twenty app:dev` whenever your objects or fields change.
Both clients are re-generated automatically by `yarn twenty dev` whenever your objects or fields change.
#### Runtime credentials in logic functions
@@ -19,7 +19,7 @@ Apps let you extend Twenty with custom objects, fields, logic functions, AI skil
## Prerequisites
- Node.js 24+ and Yarn 4
- A Twenty workspace and an API key (create one at https://app.twenty.com/settings/api-webhooks)
- Docker (for the local Twenty dev server)
## Getting Started
@@ -31,13 +31,13 @@ npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Start dev mode: automatically syncs local changes to your workspace
yarn twenty app:dev
yarn twenty dev
```
The scaffolder supports two modes for controlling which example files are included:
```bash filename="Terminal"
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill)
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent)
npx create-twenty-app@latest my-app
# Minimal: only core files (application-config.ts and default-role.ts)
@@ -63,7 +63,7 @@ yarn twenty function:execute --preInstall
yarn twenty function:execute --postInstall
# Uninstall the application from the current workspace
yarn twenty app:uninstall
yarn twenty uninstall
# Display commands' help
yarn twenty help
@@ -165,7 +165,7 @@ export default defineObject({
Later commands will add more files and folders:
- `yarn twenty app:dev` will auto-generate two typed API clients in `node_modules/twenty-sdk/generated`: `CoreApiClient` (for workspace data via `/graphql`) and `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`).
- `yarn twenty dev` will auto-generate two typed API clients in `node_modules/twenty-sdk/generated`: `CoreApiClient` (for workspace data via `/graphql`) and `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`).
- `yarn twenty entity:add` will add entity definition files under `src/` for your custom objects, functions, front components, roles, skills, and more.
## Authentication
@@ -219,13 +219,25 @@ Then add a `twenty` script:
}
```
Now you can run all commands via `yarn twenty <command>`, e.g. `yarn twenty app:dev`, `yarn twenty help`, etc.
Now you can run all commands via `yarn twenty <command>`, e.g. `yarn twenty dev`, `yarn twenty help`, etc.
## How to use a local Twenty instance
If you're already running a Twenty instance locally (e.g. via `npx nx start twenty-server`), you can connect to it instead of using Docker:
```bash filename="Terminal"
# During scaffolding — skip Docker, connect to your running instance
npx create-twenty-app@latest my-app --port 3000
# Or after scaffolding — add a remote pointing to your instance
yarn twenty remote add --local --port 3000
```
## Troubleshooting
- Authentication errors: run `yarn twenty auth:login` and ensure your API key has the required permissions.
- Cannot connect to server: verify the API URL and that the Twenty server is reachable.
- Types or client missing/outdated: restart `yarn twenty app:dev` — it auto-generates the typed client.
- Dev mode not syncing: ensure `yarn twenty app:dev` is running and that changes are not ignored by your environment.
- Types or client missing/outdated: restart `yarn twenty dev` — it auto-generates the typed client.
- Dev mode not syncing: ensure `yarn twenty dev` is running and that changes are not ignored by your environment.
Discord Help Channel: https://discord.com/channels/1130383047699738754/1130386664812982322
@@ -28,13 +28,13 @@ Publishing to npm makes your app discoverable in the Twenty marketplace. Any Twe
1. **Build your app** — the CLI compiles your TypeScript sources and generates the application manifest:
```bash filename="Terminal"
yarn twenty app:build
yarn twenty build
```
2. **Publish to npm** — push the built package to the npm registry:
```bash filename="Terminal"
npx twenty app:publish
npx twenty publish
```
### Auto-discovery
@@ -65,14 +65,14 @@ jobs:
node-version: "24"
registry-url: https://registry.npmjs.org
- run: yarn install --immutable
- run: npx twenty app:build
- run: npx twenty build
- run: npm publish --provenance --access public
working-directory: .twenty/output
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
For other CI systems (GitLab CI, CircleCI, etc.), the same three commands apply: `yarn install`, `npx twenty app:build`, then `npm publish` from `.twenty/output`.
For other CI systems (GitLab CI, CircleCI, etc.), the same three commands apply: `yarn install`, `npx twenty build`, then `npm publish` from `.twenty/output`.
<Tip>
**npm provenance** is optional but recommended. Publishing with `--provenance` adds a trust badge to your npm listing, letting users verify the package was built from a specific commit in a public CI pipeline. See the [npm provenance docs](https://docs.npmjs.com/generating-provenance-statements) for setup instructions.
@@ -87,7 +87,7 @@ For apps you don't want publicly available — proprietary tools, enterprise-onl
Build your app and deploy it to a specific server in one step:
```bash filename="Terminal"
npx twenty app:publish --server <server-url>
npx twenty publish --server <server-url>
```
Any workspace on that server can then install and upgrade the app from the **Applications** settings page.
@@ -97,7 +97,7 @@ Any workspace on that server can then install and upgrade the app from the **App
To release an update:
1. Bump the `version` field in your `package.json`
2. Push a new tarball with `npx twenty app:publish --server <server-url>`
2. Push a new tarball with `npx twenty publish --server <server-url>`
3. Workspaces on that server will see the upgrade available in their settings
<Note>
@@ -110,7 +110,7 @@ Twenty organizes apps into three categories based on how they're distributed:
| Category | How it works | Visible in marketplace? |
|----------|-------------|------------------------|
| **Development** | Local dev mode apps running via `yarn twenty app:dev`. Used for building and testing. | No |
| **Development** | Local dev mode apps running via `yarn twenty dev`. Used for building and testing. | No |
| **Published** | Apps published to npm with the `twenty-app-` prefix. Listed in the marketplace for any workspace to install. | Yes |
| **Internal** | Apps deployed via tarball to a specific server. Available only to workspaces on that server. | No |
@@ -20,19 +20,51 @@ Apps let you build and manage Twenty customizations **as code**. Instead of conf
## Prerequisites
- Node.js 24+ and Yarn 4
- A Twenty workspace and an API key (create one at https://app.twenty.com/settings/api-webhooks)
- Docker (for the local Twenty dev server)
## Getting Started
Create a new app using the official scaffolder, then authenticate and start developing:
Create a new app using the official scaffolder. It can automatically start a local Twenty instance for you:
```bash filename="Terminal"
# Scaffold a new app (includes all examples by default)
# Scaffold a new app — the CLI will offer to start a local Twenty server
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Start dev mode: automatically syncs local changes to your workspace
yarn twenty app:dev
yarn twenty dev
```
### Local Server Management
The SDK includes commands to manage a local Twenty dev server (all-in-one Docker image with PostgreSQL, Redis, server, and worker):
```bash filename="Terminal"
# Start the local server (pulls the image if needed)
yarn twenty server start
# Check server status
yarn twenty server status
# Stream server logs
yarn twenty server logs
# Stop the server
yarn twenty server stop
# Reset all data and start fresh
yarn twenty server reset
```
The local server comes pre-seeded with a workspace and user (`tim@apple.dev` / `tim@apple.dev`), so you can start developing immediately without any manual setup.
### Authentication
Connect your app to the local server using OAuth:
```bash filename="Terminal"
# Authenticate via OAuth (opens browser)
yarn twenty remote add --local
```
The scaffolder supports two modes for controlling which example files are included:
@@ -45,6 +77,18 @@ npx create-twenty-app@latest my-app
npx create-twenty-app@latest my-app --minimal
```
### How to use a local Twenty instance
If you're already running a local Twenty instance, you can connect to it instead of using Docker. Pass the port your local server is listening on (default: `3000`):
```bash filename="Terminal"
# During scaffolding
npx create-twenty-app@latest my-app --port 3000
# Or after scaffolding
yarn twenty remote add --local --port 3000
```
From here you can:
```bash filename="Terminal"
@@ -64,13 +108,13 @@ yarn twenty function:execute --preInstall
yarn twenty function:execute --postInstall
# Build the app for distribution
yarn twenty app:build
yarn twenty build
# Publish the app to npm or a Twenty server
yarn twenty app:publish
yarn twenty publish
# Uninstall the application from the current workspace
yarn twenty app:uninstall
yarn twenty uninstall
# Display commands' help
yarn twenty help
@@ -175,7 +219,7 @@ export default defineObject({
Later commands will add more files and folders:
- `yarn twenty app:dev` will auto-generate two typed API clients in `node_modules/twenty-sdk/clients`: `CoreApiClient` (for workspace data via `/graphql`) and `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`).
- `yarn twenty dev` will auto-generate two typed API clients in `node_modules/twenty-sdk/clients`: `CoreApiClient` (for workspace data via `/graphql`) and `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`).
- `yarn twenty entity:add` will add entity definition files under `src/` for your custom objects, functions, front components, roles, skills, and more.
## Authentication
@@ -824,7 +868,7 @@ export default defineFrontComponent({
Key points:
- Front components are React components that render in isolated contexts within Twenty.
- The `component` field references your React component.
- Components are built and synced automatically during `yarn twenty app:dev`.
- Components are built and synced automatically during `yarn twenty dev`.
You can create new front components in two ways:
@@ -1150,7 +1194,7 @@ You can create new agents in two ways:
### Generated typed clients
Two typed clients are auto-generated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/clients` based on your workspace schema:
Two typed clients are auto-generated by `yarn twenty dev` and stored in `node_modules/twenty-sdk/clients` based on your workspace schema:
- **`CoreApiClient`** — queries the `/graphql` endpoint for workspace data
- **`MetadataApiClient`** — queries the `/metadata` endpoint for workspace configuration and file uploads
@@ -1166,7 +1210,7 @@ const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
```
`CoreApiClient` is re-generated automatically by `yarn twenty app:dev` whenever your objects or fields change. `MetadataApiClient` ships pre-built with the SDK.
`CoreApiClient` is re-generated automatically by `yarn twenty dev` whenever your objects or fields change. `MetadataApiClient` ships pre-built with the SDK.
#### Runtime credentials in logic functions
@@ -1236,10 +1280,10 @@ Once you've developed your app with `app:dev`, use `app:build` to compile it int
```bash filename="Terminal"
# Build the app (output goes to .twenty/output/)
yarn twenty app:build
yarn twenty build
# Build and create a tarball (.tgz) for distribution
yarn twenty app:build --tarball
yarn twenty build --tarball
```
The build process:
@@ -1279,10 +1323,10 @@ Use `app:publish` to distribute your app — either to the npm registry or direc
```bash filename="Terminal"
# Publish to npm (requires npm login)
yarn twenty app:publish
yarn twenty publish
# Publish with a dist-tag (e.g. beta, next)
yarn twenty app:publish --tag beta
yarn twenty publish --tag beta
```
This builds the app and runs `npm publish` from the `.twenty/output/` directory. The published package can then be installed from the Twenty marketplace by any workspace.
@@ -1291,7 +1335,7 @@ This builds the app and runs `npm publish` from the `.twenty/output/` directory.
```bash filename="Terminal"
# Publish directly to a Twenty server
yarn twenty app:publish --server https://app.twenty.com
yarn twenty publish --server https://app.twenty.com
```
This builds the app with a tarball, uploads it to the server via the `uploadAppTarball` GraphQL mutation, and triggers installation in one step. This is useful for private deployments or testing against a specific server.
@@ -1355,13 +1399,13 @@ Then add a `twenty` script:
}
```
Now you can run all commands via `yarn twenty <command>`, e.g. `yarn twenty app:dev`, `yarn twenty help`, etc.
Now you can run all commands via `yarn twenty <command>`, e.g. `yarn twenty dev`, `yarn twenty help`, etc.
## Troubleshooting
- Authentication errors: run `yarn twenty auth:login` and ensure your API key has the required permissions.
- Cannot connect to server: verify the API URL and that the Twenty server is reachable.
- Types or client missing/outdated: restart `yarn twenty app:dev` — it auto-generates the typed client.
- Dev mode not syncing: ensure `yarn twenty app:dev` is running and that changes are not ignored by your environment.
- Types or client missing/outdated: restart `yarn twenty dev` — it auto-generates the typed client.
- Dev mode not syncing: ensure `yarn twenty dev` is running and that changes are not ignored by your environment.
Discord Help Channel: https://discord.com/channels/1130383047699738754/1130386664812982322
@@ -157,7 +157,9 @@ plugins: [
Run `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = 'you@yourdomain.com';` in database container to get access to admin panel.
#### When running workflow, workflow run fails with "Logic function execution is disabled. Set LOGIC_FUNCTION_TYPE to LOCAL or LAMBDA to enable."
In production, logic functions are disabled by default. Set the `LOGIC_FUNCTION_TYPE` environment variable to `LOCAL` or `LAMBDA` to enable them. This can be configured via environment variables or through the admin panel database variables. See the [Logic Functions setup guide](/developers/self-host/capabilities/setup#logic-functions-available-drivers) for details.
### 1-click Docker compose
+29 -15
View File
@@ -175,7 +175,8 @@
"user-guide/workflows/how-tos/crm-automations/formula-fields",
"user-guide/workflows/how-tos/crm-automations/display-related-record-data",
"user-guide/workflows/how-tos/crm-automations/closed-won-automations",
"user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
"user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
"user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
]
},
{
@@ -621,7 +622,8 @@
"l/fr/user-guide/workflows/how-tos/crm-automations/formula-fields",
"l/fr/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
"l/fr/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
"l/fr/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
"l/fr/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
"l/fr/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
]
},
{
@@ -1067,7 +1069,8 @@
"l/ar/user-guide/workflows/how-tos/crm-automations/formula-fields",
"l/ar/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
"l/ar/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
"l/ar/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
"l/ar/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
"l/ar/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
]
},
{
@@ -1513,7 +1516,8 @@
"l/cs/user-guide/workflows/how-tos/crm-automations/formula-fields",
"l/cs/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
"l/cs/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
"l/cs/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
"l/cs/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
"l/cs/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
]
},
{
@@ -1699,7 +1703,7 @@
"l/cs/developers/extend/api",
"l/cs/developers/extend/webhooks",
{
"group": "Apps",
"group": "Aplikace",
"pages": [
"l/cs/developers/extend/apps/getting-started",
"l/cs/developers/extend/apps/building",
@@ -1959,7 +1963,8 @@
"l/de/user-guide/workflows/how-tos/crm-automations/formula-fields",
"l/de/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
"l/de/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
"l/de/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
"l/de/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
"l/de/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
]
},
{
@@ -2405,7 +2410,8 @@
"l/es/user-guide/workflows/how-tos/crm-automations/formula-fields",
"l/es/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
"l/es/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
"l/es/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
"l/es/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
"l/es/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
]
},
{
@@ -2851,7 +2857,8 @@
"l/it/user-guide/workflows/how-tos/crm-automations/formula-fields",
"l/it/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
"l/it/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
"l/it/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
"l/it/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
"l/it/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
]
},
{
@@ -3297,7 +3304,8 @@
"l/ja/user-guide/workflows/how-tos/crm-automations/formula-fields",
"l/ja/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
"l/ja/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
"l/ja/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
"l/ja/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
"l/ja/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
]
},
{
@@ -3743,7 +3751,8 @@
"l/ko/user-guide/workflows/how-tos/crm-automations/formula-fields",
"l/ko/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
"l/ko/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
"l/ko/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
"l/ko/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
"l/ko/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
]
},
{
@@ -4189,7 +4198,8 @@
"l/pt/user-guide/workflows/how-tos/crm-automations/formula-fields",
"l/pt/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
"l/pt/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
"l/pt/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
"l/pt/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
"l/pt/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
]
},
{
@@ -4635,7 +4645,8 @@
"l/ro/user-guide/workflows/how-tos/crm-automations/formula-fields",
"l/ro/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
"l/ro/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
"l/ro/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
"l/ro/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
"l/ro/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
]
},
{
@@ -5081,7 +5092,8 @@
"l/ru/user-guide/workflows/how-tos/crm-automations/formula-fields",
"l/ru/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
"l/ru/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
"l/ru/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
"l/ru/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
"l/ru/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
]
},
{
@@ -5527,7 +5539,8 @@
"l/tr/user-guide/workflows/how-tos/crm-automations/formula-fields",
"l/tr/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
"l/tr/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
"l/tr/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
"l/tr/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
"l/tr/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
]
},
{
@@ -5973,7 +5986,8 @@
"l/zh/user-guide/workflows/how-tos/crm-automations/formula-fields",
"l/zh/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
"l/zh/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
"l/zh/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
"l/zh/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
"l/zh/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
]
},
{
Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 931 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

@@ -563,7 +563,7 @@ export default defineFrontComponent({
* المكوّنات الأمامية هي مكوّنات React تُعرَض ضمن سياقات معزولة داخل Twenty.
* يشير الحقل `component` إلى مكوّن React الخاص بك.
* يتم بناء المكوّنات ومزامنتها تلقائيًا أثناء `yarn twenty app:dev`.
* يتم بناء المكوّنات ومزامنتها تلقائيًا أثناء `yarn twenty dev`.
يمكنك إنشاء مكوّنات أمامية جديدة بطريقتين:
@@ -607,7 +607,7 @@ export default defineSkill({
### عملاء مُولَّدون مضبوطو الأنواع
يتم توليد عميلين مضبوطي الأنواع تلقائيًا بواسطة `yarn twenty app:dev` وتخزينهما في `node_modules/twenty-sdk/generated` استنادًا إلى مخطط مساحة العمل لديك:
يتم توليد عميلين مضبوطي الأنواع تلقائيًا بواسطة `yarn twenty dev` وتخزينهما في `node_modules/twenty-sdk/generated` استنادًا إلى مخطط مساحة العمل لديك:
* **`CoreApiClient`** — يُجري استعلامات إلى نقطة النهاية `/graphql` للحصول على بيانات مساحة العمل
* **`MetadataApiClient`** — يستعلم عن نقطة النهاية `/metadata` لتكوين مساحة العمل وتحميل الملفات.
@@ -622,7 +622,7 @@ const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
```
يُعاد توليد كلا العميلين تلقائيًا بواسطة `yarn twenty app:dev` كلما تغيّرت كائناتك أو حقولك.
يُعاد توليد كلا العميلين تلقائيًا بواسطة `yarn twenty dev` كلما تغيّرت كائناتك أو حقولك.
#### بيانات الاعتماد وقت التشغيل في الدوال المنطقية
@@ -20,7 +20,7 @@ description: أنشئ أول تطبيق Twenty خلال دقائق.
## المتطلبات الأساسية
* Node.js 24+ وYarn 4
* مساحة عمل Twenty ومفتاح واجهة برمجة التطبيقات (أنشئ واحدًا على https://app.twenty.com/settings/api-webhooks)
* Docker (لخادم تطوير Twenty المحلي)
## البدء
@@ -32,13 +32,13 @@ npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Start dev mode: automatically syncs local changes to your workspace
yarn twenty app:dev
yarn twenty dev
```
يدعم المُهيئ وضعين للتحكم في ملفات الأمثلة التي سيتم تضمينها:
```bash filename="Terminal"
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill)
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent)
npx create-twenty-app@latest my-app
# Minimal: only core files (application-config.ts and default-role.ts)
@@ -64,7 +64,7 @@ yarn twenty function:execute --preInstall
yarn twenty function:execute --postInstall
# Uninstall the application from the current workspace
yarn twenty app:uninstall
yarn twenty uninstall
# Display commands' help
yarn twenty help
@@ -167,7 +167,7 @@ export default defineObject({
ستضيف الأوامر اللاحقة مزيدًا من الملفات والمجلدات:
* سيقوم `yarn twenty app:dev` بتوليد عميلين API مضبوطي الأنواع تلقائيًا في `node_modules/twenty-sdk/generated`: `CoreApiClient` (لبيانات مساحة العمل عبر `/graphql`) و`MetadataApiClient` (لتكوين مساحة العمل وتحميل الملفات عبر `/metadata`).
* `yarn twenty dev` will auto-generate two typed API clients in `node_modules/twenty-sdk/generated`: `CoreApiClient` (for workspace data via `/graphql`) and `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`).
* `yarn twenty entity:add` سيضيف ملفات تعريف الكيانات ضمن `src/` لكائناتك المخصّصة، والوظائف، ومكوّنات الواجهة الأمامية، والأدوار، والمهارات، وغير ذلك.
## المصادقة
@@ -221,13 +221,25 @@ yarn add -D twenty-sdk
}
```
الآن يمكنك تشغيل جميع الأوامر عبر `yarn twenty <command>`، مثلًا: `yarn twenty app:dev`، `yarn twenty help`، إلخ.
Now you can run all commands via `yarn twenty <command>`, e.g. `yarn twenty dev`, `yarn twenty help`, etc.
## كيفية استخدام مثيل محلي من Twenty
إذا كنت تقوم بتشغيل مثيل محلي من Twenty بالفعل (على سبيل المثال عبر `npx nx start twenty-server`)، فيمكنك الاتصال به بدلًا من استخدام Docker:
```bash filename="Terminal"
# During scaffolding — skip Docker, connect to your running instance
npx create-twenty-app@latest my-app --port 3000
# Or after scaffolding — add a remote pointing to your instance
yarn twenty remote add --local --port 3000
```
## استكشاف الأخطاء وإصلاحها
* أخطاء المصادقة: شغّل `yarn twenty auth:login` وتأكد من أن مفتاح واجهة برمجة التطبيقات لديك يمتلك الأذونات المطلوبة.
* يتعذّر الاتصال بالخادم: تحقق من عنوان URL لواجهة برمجة التطبيقات وأن خادم Twenty قابل للوصول.
* الأنواع أو العميل مفقود/قديم: أعد تشغيل `yarn twenty app:dev` — فهو ينشئ العميل مضبوط الأنواع بشكل تلقائي.
* وضع التطوير لا يزامن: تأكد من أن `yarn twenty app:dev` قيد التشغيل وأن التغييرات ليست متجاهلة من بيئتك.
* Types or client missing/outdated: restart `yarn twenty dev` — it auto-generates the typed client.
* Dev mode not syncing: ensure `yarn twenty dev` is running and that changes are not ignored by your environment.
قناة المساعدة على Discord: https://discord.com/channels/1130383047699738754/1130386664812982322
@@ -28,13 +28,13 @@ description: وزّع تطبيق Twenty الخاص بك على سوق Twenty أ
1. **قم ببناء تطبيقك** — تقوم أداة CLI بتجميع مصادر TypeScript الخاصة بك وإنشاء ملف بيان التطبيق:
```bash filename="Terminal"
yarn twenty app:build
yarn twenty build
```
2. **النشر على npm** — ادفع الحزمة المبنية إلى سجل npm:
```bash filename="Terminal"
npx twenty app:publish
npx twenty publish
```
### الاكتشاف التلقائي
@@ -65,14 +65,14 @@ jobs:
node-version: "24"
registry-url: https://registry.npmjs.org
- run: yarn install --immutable
- run: npx twenty app:build
- run: npx twenty build
- run: npm publish --provenance --access public
working-directory: .twenty/output
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
بالنسبة لأنظمة CI الأخرى (GitLab CI، CircleCI، إلخ)، تنطبق الأوامر الثلاثة نفسها: `yarn install`، ثم `npx twenty app:build`، ثم `npm publish` من `.twenty/output`.
For other CI systems (GitLab CI, CircleCI, etc.), the same three commands apply: `yarn install`, `npx twenty build`, then `npm publish` from `.twenty/output`.
<Tip>
**npm provenance** اختياري ولكنه موصى به. يضيف النشر باستخدام `--provenance` شارة ثقة إلى إدراجك على npm، مما يتيح للمستخدمين التحقق من أن الحزمة تم بناؤها من التزام محدد ضمن خط أنابيب CI عام. راجع [وثائق npm provenance](https://docs.npmjs.com/generating-provenance-statements) للحصول على تعليمات الإعداد.
@@ -87,7 +87,7 @@ jobs:
قم ببناء تطبيقك وانشره إلى خادم محدد في خطوة واحدة:
```bash filename="Terminal"
npx twenty app:publish --server <server-url>
npx twenty publish --server <server-url>
```
يمكن لأي مساحة عمل على ذلك الخادم بعدها تثبيت التطبيق وترقيته من صفحة الإعدادات **التطبيقات**.
@@ -97,7 +97,7 @@ npx twenty app:publish --server <server-url>
لطرح تحديث:
1. ارفع قيمة الحقل `version` في ملف `package.json`
2. أرسل tarball جديدًا باستخدام `npx twenty app:publish --server <server-url>`
2. Push a new tarball with `npx twenty publish --server <server-url>`
3. سترى مساحات العمل على ذلك الخادم الترقية متاحة في إعداداتها
<Note>
@@ -110,7 +110,7 @@ npx twenty app:publish --server <server-url>
| الفئة | كيف يعمل | مرئي في سوق Twenty؟ |
| ----------- | ---------------------------------------------------------------------------------------------------- | ------------------- |
| **التطوير** | تطبيقات وضع التطوير المحلي التي تعمل عبر `yarn twenty app:dev`. تُستخدم للبناء والاختبار. | لا |
| **التطوير** | Local dev mode apps running via `yarn twenty dev`. تُستخدم للبناء والاختبار. | لا |
| **منشور** | تطبيقات منشورة على npm مع البادئة `twenty-app-`. مدرجة في سوق Twenty لتتمكن أي مساحة عمل من تثبيتها. | نعم |
| **داخلي** | تطبيقات منشورة عبر tarball إلى خادم محدد. متاحة فقط لمساحات العمل على ذلك الخادم. | لا |
@@ -21,19 +21,51 @@ description: أنشئ وأدِر تخصيصات Twenty على هيئة كود.
## المتطلبات الأساسية
* Node.js 24+ وYarn 4
* مساحة عمل Twenty ومفتاح واجهة برمجة التطبيقات (أنشئ واحدًا على https://app.twenty.com/settings/api-webhooks)
* Docker (لخادم تطوير Twenty المحلي)
## البدء
أنشئ تطبيقًا جديدًا باستخدام المُهيئ الرسمي، ثم قم بالمصادقة وابدأ التطوير:
أنشئ تطبيقًا جديدًا باستخدام المولّد الرسمي. يمكنه بدء مثيل محلي من Twenty تلقائيًا لك:
```bash filename="Terminal"
# إنشاء تطبيق جديد (يتضمن جميع الأمثلة افتراضيًا)
# إنشاء تطبيق جديد — ستعرض واجهة سطر الأوامر خيار بدء خادم Twenty محلي
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# ابدأ وضع التطوير: يُزامن التغييرات المحلية تلقائيًا مع مساحة العمل الخاصة بك
yarn twenty app:dev
yarn twenty dev
```
### إدارة الخادم المحلي
يتضمن SDK أوامر لإدارة خادم تطوير Twenty محلي (صورة Docker متكاملة تتضمن PostgreSQL وRedis والخادم والعامل):
```bash filename="Terminal"
# ابدأ الخادم المحلي (يسحب الصورة إذا لزم الأمر)
yarn twenty server start
# تحقّق من حالة الخادم
yarn twenty server status
# بثّ سجلات الخادم
yarn twenty server logs
# أوقف الخادم
yarn twenty server stop
# أعد ضبط جميع البيانات وابدأ من جديد
yarn twenty server reset
```
يأتي الخادم المحلي مهيأً مسبقًا بمساحة عمل ومستخدم (`tim@apple.dev` / `tim@apple.dev`)، بحيث يمكنك البدء في التطوير فورًا دون أي إعداد يدوي.
### المصادقة
وصّل تطبيقك بالخادم المحلي باستخدام OAuth:
```bash filename="Terminal"
# المصادقة عبر OAuth (يفتح المتصفح)
yarn twenty remote add --local
```
يدعم المُنشئ وضعين للتحكم في ملفات الأمثلة التي سيتم تضمينها:
@@ -46,34 +78,46 @@ npx create-twenty-app@latest my-app
npx create-twenty-app@latest my-app --minimal
```
### كيفية استخدام مثيل محلي من Twenty
إذا كنت تقوم بتشغيل مثيل محلي من Twenty بالفعل، فيمكنك الاتصال به بدلًا من استخدام Docker. مرِّر المنفذ الذي يستمع عليه الخادم المحلي لديك (القيمة الافتراضية: `3000`):
```bash filename="Terminal"
# During scaffolding
npx create-twenty-app@latest my-app --port 3000
# Or after scaffolding
yarn twenty remote add --local --port 3000
```
من هنا يمكنك:
```bash filename="Terminal"
# أضف كيانًا جديدًا إلى تطبيقك (موجّه)
# Add a new entity to your application (guided)
yarn twenty entity:add
# راقب سجلات وظائف تطبيقك
# Watch your application's function logs
yarn twenty function:logs
# نفّذ وظيفة بالاسم
# Execute a function by name
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# نفّذ دالة ما قبل التثبيت
# Execute the pre-install function
yarn twenty function:execute --preInstall
# نفّذ دالة ما بعد التثبيت
# Execute the post-install function
yarn twenty function:execute --postInstall
# ابنِ التطبيق للتوزيع
yarn twenty app:build
# Build the app for distribution
yarn twenty build
# انشر التطبيق إلى npm أو إلى خادم Twenty
yarn twenty app:publish
# Publish the app to npm or a Twenty server
yarn twenty publish
# أزل تثبيت التطبيق من مساحة العمل الحالية
yarn twenty app:uninstall
# Uninstall the application from the current workspace
yarn twenty uninstall
# اعرض مساعدة الأوامر
# Display commands' help
yarn twenty help
```
@@ -177,7 +221,7 @@ export default defineObject({
ستضيف الأوامر اللاحقة مزيدًا من الملفات والمجلدات:
* سيقوم `yarn twenty app:dev` بتوليد عميلين API مضبوطي الأنواع تلقائيًا في `node_modules/twenty-sdk/clients`: `CoreApiClient` (لبيانات مساحة العمل عبر `/graphql`) و`MetadataApiClient` (لتكوين مساحة العمل وتحميل الملفات عبر `/metadata`).
* `yarn twenty dev` will auto-generate two typed API clients in `node_modules/twenty-sdk/clients`: `CoreApiClient` (for workspace data via `/graphql`) and `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`).
* `yarn twenty entity:add` سيضيف ملفات تعريف الكيانات ضمن `src/` لكائناتك المخصّصة، والوظائف، ومكوّنات الواجهة الأمامية، والأدوار، والمهارات، وغير ذلك.
## المصادقة
@@ -836,7 +880,7 @@ export default defineFrontComponent({
* المكوّنات الأمامية هي مكوّنات React تُعرَض ضمن سياقات معزولة داخل Twenty.
* يشير الحقل `component` إلى مكوّن React الخاص بك.
* يتم بناء المكوّنات ومزامنتها تلقائيًا أثناء `yarn twenty app:dev`.
* Components are built and synced automatically during `yarn twenty dev`.
يمكنك إنشاء مكوّنات أمامية جديدة بطريقتين:
@@ -1164,7 +1208,7 @@ export default defineAgent({
### عملاء مُولَّدون مضبوطو الأنواع
يتم توليد عميلين مضبوطي الأنواع تلقائيًا بواسطة `yarn twenty app:dev` وتخزينهما في `node_modules/twenty-sdk/clients` استنادًا إلى مخطط مساحة العمل لديك:
Two typed clients are auto-generated by `yarn twenty dev` and stored in `node_modules/twenty-sdk/clients` based on your workspace schema:
* **`CoreApiClient`** — يُجري استعلامات إلى نقطة النهاية `/graphql` للحصول على بيانات مساحة العمل
* **`MetadataApiClient`** — يستعلم عن نقطة النهاية `/metadata` لتكوين مساحة العمل وتحميل الملفات.
@@ -1180,7 +1224,7 @@ const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
```
`CoreApiClient` يُعاد توليده تلقائيًا بواسطة `yarn twenty app:dev` كلما تغيّرت كائناتك أو حقولك. `MetadataApiClient` يأتي مُجهزًا مسبقًا مع SDK.
`CoreApiClient` is re-generated automatically by `yarn twenty dev` whenever your objects or fields change. `MetadataApiClient` يأتي مُجهزًا مسبقًا مع SDK.
#### بيانات الاعتماد وقت التشغيل في الوظائف المنطقية
@@ -1251,11 +1295,11 @@ uploadFile(
بمجرد أن تطوّر تطبيقك باستخدام `app:dev`، استخدم `app:build` لإنشاء حزمة قابلة للتوزيع منه.
```bash filename="Terminal"
# ابنِ التطبيق (الإخراج يذهب إلى .twenty/output/)
yarn twenty app:build
# Build the app (output goes to .twenty/output/)
yarn twenty build
# ابنِ وأنشئ ملف tarball (.tgz) للتوزيع
yarn twenty app:build --tarball
# Build and create a tarball (.tgz) for distribution
yarn twenty build --tarball
```
عملية البناء:
@@ -1295,10 +1339,10 @@ yarn twenty app:build --tarball
```bash filename="Terminal"
# Publish to npm (requires npm login)
yarn twenty app:publish
yarn twenty publish
# Publish with a dist-tag (e.g. beta, next)
yarn twenty app:publish --tag beta
yarn twenty publish --tag beta
```
يقوم هذا ببناء التطبيق وتشغيل `npm publish` من دليل `.twenty/output/`. بعد ذلك يمكن تثبيت الحزمة المنشورة من سوق Twenty بواسطة أي مساحة عمل.
@@ -1307,7 +1351,7 @@ yarn twenty app:publish --tag beta
```bash filename="Terminal"
# Publish directly to a Twenty server
yarn twenty app:publish --server https://app.twenty.com
yarn twenty publish --server https://app.twenty.com
```
يقوم هذا ببناء التطبيق مع أرشيف tar، ويرفعه إلى الخادم عبر العملية `uploadAppTarball` في GraphQL، ويبدأ التثبيت في خطوة واحدة. يكون هذا مفيدًا لعمليات النشر الخاصة أو للاختبار مقابل خادم محدّد.
@@ -1371,13 +1415,13 @@ yarn add -D twenty-sdk
}
```
الآن يمكنك تشغيل جميع الأوامر عبر `yarn twenty <command>`، مثلًا: `yarn twenty app:dev`، `yarn twenty help`، إلخ.
Now you can run all commands via `yarn twenty <command>`, e.g. `yarn twenty dev`, `yarn twenty help`, etc.
## استكشاف الأخطاء وإصلاحها
* أخطاء المصادقة: شغّل `yarn twenty auth:login` وتأكد من أن مفتاح واجهة برمجة التطبيقات لديك يمتلك الأذونات المطلوبة.
* يتعذّر الاتصال بالخادم: تحقق من عنوان URL لواجهة البرمجة وأن خادم Twenty قابل للوصول.
* الأنواع أو العميل مفقود/قديم: أعد تشغيل `yarn twenty app:dev` — فهو ينشئ العميل مضبوط الأنواع بشكل تلقائي.
* وضع التطوير لا يزامن: تأكد من أن `yarn twenty app:dev` قيد التشغيل وأن التغييرات ليست متجاهلة من بيئتك.
* Types or client missing/outdated: restart `yarn twenty dev` — it auto-generates the typed client.
* Dev mode not syncing: ensure `yarn twenty dev` is running and that changes are not ignored by your environment.
قناة المساعدة على Discord: https://discord.com/channels/1130383047699738754/1130386664812982322
@@ -166,6 +166,10 @@ plugins: [
قم بتشغيل `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = 'you@yourdomain.com';` في حاوية قاعدة البيانات للحصول على الوصول إلى لوحة الإدارة.
#### عند تشغيل سير العمل، يفشل تشغيل سير العمل مع الرسالة "تم تعطيل تنفيذ دوال المنطق. عيّن LOGIC_FUNCTION_TYPE إلى LOCAL أو LAMBDA لتمكين ذلك."
في بيئة الإنتاج، يتم تعطيل دوال المنطق افتراضيًا. قم بتعيين متغير البيئة `LOGIC_FUNCTION_TYPE` إلى `LOCAL` أو `LAMBDA` لتمكينها. يمكن تكوين ذلك عبر متغيرات البيئة أو من خلال متغيرات قاعدة البيانات في لوحة الإدارة. راجع [دليل إعداد دوال المنطق](/l/ar/developers/self-host/capabilities/setup#logic-functions-available-drivers) للحصول على التفاصيل.
### Docker compose بنقرة واحدة
#### غير قادر على تسجيل الدخول
@@ -0,0 +1,121 @@
---
title: الرد التلقائي على رسائل البريد الإلكتروني الواردة
description: أنشئ سير عمل يستخدم الذكاء الاصطناعي لفرز رسائل البريد الإلكتروني الواردة وإرسال ردود ضمن سلسلة المحادثة تلقائيًا.
---
استجب لرسائل البريد الإلكتروني الواردة خلال ثوانٍ — وليس ساعات. يستخدم سير العمل هذا وكيلاً للذكاء الاصطناعي لتصفية الضوضاء (النشرات الإخبارية، الرسائل غير المرغوب فيها، الردود التلقائية) وصياغة رد مخصص للرسائل الحقيقية، ثم يرسله كرد مُسلسل داخل المحادثة الأصلية.
## كيف يعمل تسلسل رسائل البريد الإلكتروني
تحمل كل رسالة بريد إلكتروني ترويسة `Message-ID` مخفية — بصمة فريدة يعيّنها خادم بريد المرسل. عند الرد على بريد إلكتروني، يقوم عميل البريد لديك بتعيين ترويسة `In-Reply-To` التي تُشير إلى تلك البصمة. بهذه الطريقة يقوم Gmail وOutlook وكل عميل آخر بتجميع الرسائل في سلاسل المحادثات.
في Twenty، تُخزَّن تلك البصمة كـ `headerMessageId` على كائن Message. يلتقطه سير العمل لديك ويمرّره إلى حقل In-Reply-To لإجراء Send Email.
## بناء سير العمل
### الخطوة 1: إنشاء سير عمل جديد
توجّه إلى **Settings -> Workflows** وانقر **+ New Workflow**.
### الخطوة 2: التشغيل عند الرسائل الواردة
اختر **When a Record is Created** ثم **Messages**.
في كل مرة يصل فيها بريد إلكتروني إلى Twenty، يُشغَّل هذا.
<img src="/images/user-guide/workflows/auto-reply/workflow-overview.png" style={{width:'100%'}} />
### الخطوة 3: البحث عن المُرسِل
أضِف إجراء **Search Records**.
عنوان المُرسِل ليس في الرسالة نفسها — بل في سجل Message Participant المرتبط.
| الحقل | القيمة |
| ---------- | ---------------------------------- |
| **الكائن** | المشاركون في الرسالة |
| **تصفية** | Message **يساوي** `{{trigger.id}}` |
| **تصفية** | Role **يساوي** From |
| **الحد** | 1 |
سيمنحك هذا بريد المُرسِل الإلكتروني في `handle` واسمه في `displayName`.
<img src="/images/user-guide/workflows/auto-reply/find-sender.png" style={{width:'100%'}} />
### الخطوة 4: الفرز بالذكاء الاصطناعي وصياغة الرد
أضِف إجراء **AI Agent**. تقوم هذه الخطوة الواحدة بأمرين: تقرر ما إذا كان البريد الإلكتروني يستحق الرد، وإن كان كذلك، تصوغ ردًا.
استخدم موجهًا مثل:
```
You are an email triage assistant for a sales team. Read the following
inbound email and decide if it deserves a reply.
Subject: {{trigger.subject}}
Body: {{trigger.text}}
From: {{Find Sender.first.displayName}} ({{Find Sender.first.handle}})
If this email is spam, a newsletter, an automated notification, or
otherwise does not need a human reply, respond with exactly: SKIP
Otherwise, write a short, professional reply (3-4 sentences max) that:
- Acknowledges their specific message
- Lets them know someone from the team will follow up shortly
- Is warm but not overly casual
Respond with only the reply text, no subject line or greeting prefix.
```
يُنتِج AI Agent استجابته في الحقل `response` الذي يمكن للخطوات التالية الرجوع إليه.
<img src="/images/user-guide/workflows/auto-reply/ai-triage.png" style={{width:'100%'}} />
### الخطوة 5: التفريع بناءً على قرار الذكاء الاصطناعي
أضِف إجراء **If/Else** للتحقق مما إذا كان الذكاء الاصطناعي قرر الرد أم التخطي.
| الحقل | القيمة |
| ------------------ | ------------------------------------------- |
| **الشرط** | AI Agent `response` **لا يحتوي على** `SKIP` |
| **إذا كان صحيحًا** | المتابعة إلى Send Email |
| **وإلا** | لا تفعل شيئًا (ينتهي سير العمل) |
يتم تجاهل الرسائل غير المرغوب فيها والنشرات البريدية والرسائل المُنشأة تلقائيًا. كل ما عدا ذلك ينتقل إلى الخطوة التالية.
<img src="/images/user-guide/workflows/auto-reply/should-reply.png" style={{width:'100%'}} />
### الخطوة 6: إرسال رد ضمن سلسلة المحادثة
أضِف إجراء **Send Email** على فرع "if true". انقر **Advanced options** ثم **Add In-Reply-To**.
| الحقل | القيمة |
| --------------- | -------------------------------------- |
| **إلى** | `{{Find Sender.first.handle}}` |
| **الموضوع** | `Re: {{trigger.subject}}` |
| **المحتوى** | `{{AI Triage & Draft Reply.response}}` |
| **In-Reply-To** | `{{trigger.headerMessageId}}` |
حقل In-Reply-To هو ما يجعل هذا ردًا بدلًا من محادثة جديدة. سيراه المستلِم ضمن سلسلة تحت البريد الأصلي في Gmail أو Outlook أو أي عميل آخر.
<img src="/images/user-guide/workflows/auto-reply/send-email.png" style={{width:'100%'}} />
<Tip>
**In-Reply-To** يتوقّع `message.headerMessageId` من المشغّل — إنها البصمة الفريدة للبريد الإلكتروني، وليست عنوان المستلم. إذا تركته فارغًا، فسيُرسَل البريد الإلكتروني على أي حال، ولكن كرسالة مستقلة.
</Tip>
<Warning>
يستخدم Gmail سطر الموضوع لتجميع الرسائل في سلاسل المحادثات. يجب أن يبدأ الموضوع بـ `Re:` (بما في ذلك النقطتان والمسافة) ليعرض Gmail الرد داخل سلسلة المحادثة الأصلية. بدون ذلك، سيظهر الرد كمحادثة منفصلة — حتى إذا تم تعيين ترويسة In-Reply-To بشكل صحيح.
</Warning>
### الخطوة 7: الاختبار والتفعيل
اضغط **Test**، ثم تحقّق من عميل البريد لديك. يجب أن يظهر الرد متداخلًا تحت الرسالة الأصلية.
فعِّل عندما تكون راضيًا عنه.
## أفكار للبناء عليها
* **الرد على كبار الشخصيات فقط** — أضِف فرعًا يتحقق من مجال المُرسِل أو مما إذا كان موجودًا كجهة اتصال في Twenty
* **التوجيه حسب النية** — استخدم موجهات AI Agent منفصلة للتعامل مع استفسارات المبيعات بشكل مختلف عن طلبات الدعم
* **الإثراء قبل الرد** — أضِف خطوة Search Records لجلب شركة المُرسِل أو سجل الصفقات إلى الموجه الخاص بالذكاء الاصطناعي للحصول على ردود أكثر تخصيصًا
@@ -77,7 +77,7 @@ To avoid unnecessary [re-renders](/l/cs/developers/contribute/capabilities/front
### Správa stavu
[Jotai](https://jotai.org/) handles state management.
[Jotai](https://jotai.org/) zajišťuje správu stavu.
Podívejte se na [osvědčené postupy](/l/cs/developers/contribute/capabilities/frontend-development/best-practices-front#state-management) pro více informací o správě stavu.
@@ -8,7 +8,7 @@ Cílem zde je mít konzistentní kódovou základnu, která je snadno čitelná
Pro toto je lepší být trochu více rozvláčný než být příliš stručný.
Vždy mějte na paměti, že lidé čtou kód častěji, než ho píší, zvláště u projektu s otevřeným zdrojovým kódem, kde může kdokoli přispět.
Vždy mějte na paměti, že lidé čtou kód častěji než ho píší, zejména u projektu s otevřeným zdrojovým kódem, kde může kdokoli přispět.
Existuje mnoho pravidel, která zde nejsou definována, ale která jsou automaticky kontrolována lintery.
@@ -150,7 +150,7 @@ type MyType = {
### Používejte textové literály místo výčtů
[Textové literály](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types) jsou preferovaný způsob pro zpracování hodnot podobných výčtům v TypeScriptu. Jsou snadněji rozšířitelné pomocí Pick a Omit a nabízejí lepší uživatelský zážitek, zejména s automatickým dokončováním kódu.
[Textové literály](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types) jsou preferovaný způsob pro zpracování hodnot podobných výčtům v TypeScriptu. Snadněji se rozšiřují pomocí Pick a Omit a nabízejí lepší vývojářskou zkušenost, zejména s automatickým dokončováním kódu.
Proč TypeScript doporučuje vyhnout se výčtům zjistíte [zde](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#enums).
@@ -6,7 +6,7 @@ description: Průvodce pro přispěvatele (nebo zvídavé vývojáře), kteří
## Předpoklady
<Tabs>
<Tab title="Linux a MacOS">
<Tab title="Linux a macOS">
Než nainstalujete a použijete Twenty, ujistěte se, že máte na svém počítači nainstalovány následující balíčky:
* [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
@@ -30,7 +30,7 @@ wsl --install
```
Nyní byste měli vidět výzvu k restartování počítače. Pokud ne, restartujte jej ručně.
Při restartu se otevře okno PowerShell a nainstaluje Ubuntu. Tento proces může chvíli trvat.
Při restartu se otevře okno PowerShellu a nainstaluje Ubuntu. Tento proces může chvíli trvat.
Zobrazí se výzva k vytvoření uživatelského jména a hesla pro vaši instalaci Ubuntu.
2. Nainstalujte a nastavte git
@@ -102,8 +102,8 @@ Všechny příkazy v následujících krocích byste měli provádět z kořene
<Tabs>
<Tab title="Linux">
**Možnost 1 (doporučeno):** Pro lokalní vytvoření databáze:
Použijte následující odkaz pro instalaci PostgreSQL na vašem Linuxovém stroji: [Instalace PostgreSQL](https://www.postgresql.org/download/linux/)
**Možnost 1 (doporučeno):** Pro lokální vytvoření databáze:
Použijte následující odkaz pro instalaci PostgreSQL na vašem linuxovém stroji: [Instalace PostgreSQL](https://www.postgresql.org/download/linux/)
```bash
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
```
@@ -129,8 +129,8 @@ Všechny příkazy v následujících krocích byste měli provádět z kořene
brew services list
```
Instalátor nemusí automaticky vytvořit uživatele `postgres` při instalaci
přes Homebrew na MacOS. Místo toho vytvoří PostgreSQL roli, která odpovídá vašemu uživatelskému jménu v MacOS (např. "john").
Instalátor nemusí ve výchozím nastavení vytvořit uživatele `postgres` při instalaci
přes Homebrew na macOS. Místo toho vytvoří PostgreSQL roli, která odpovídá vašemu uživatelskému jménu v MacOS (např. "john").
Pro zkontrolování a vytvoření uživatele `postgres`, pokud je to nutné, postupujte takto:
```bash
# Připojit se k PostgreSQL
@@ -172,8 +172,8 @@ Všechny příkazy v následujících krocích byste měli provádět z kořene
<Tab title="Windows (WSL)">
Všechny následující kroky je třeba provádět v terminálu WSL (v rámci vaší virtuálního stroje).
**Možnost 1:** Pro lokalní vytvoření PostgreSQL:
Použijte následující odkaz pro instalaci PostgreSQL na vašem Linuxovém virtuálním stroji: [Instalace PostgreSQL](https://www.postgresql.org/download/linux/)
**Možnost 1:** Pro lokální zprovoznění PostgreSQL:
Použijte následující odkaz pro instalaci PostgreSQL na vašem linuxovém virtuálním stroji: [Instalace PostgreSQL](https://www.postgresql.org/download/linux/)
```bash
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
```
@@ -188,7 +188,9 @@ Všechny příkazy v následujících krocích byste měli provádět z kořene
</Tab>
</Tabs>
Nyní můžete přistupovat k databázi na [localhost:5432](localhost:5432), s uživatelem `postgres` a heslem `postgres`.
Nyní můžete přistupovat k databázi na adrese `localhost:5432`.
Pokud jste použili výše uvedenou možnost Dockeru, výchozí přihlašovací údaje jsou uživatelské jméno `postgres` a heslo `postgres`. U nativních instalací PostgreSQL použijte přihlašovací údaje a role nakonfigurované na vašem počítači.
## Krok 4: Nastavení Redis databáze (cache)
@@ -209,8 +211,10 @@ Twenty vyžaduje ke svému provozu Redis cache pro zajištění nejlepšího vý
```bash
brew install redis
```
Spusťte redis server:
`brew services start redis`
Spusťte svůj server Redis:
```bash
brew services start redis
```
**Možnost 2:** Pokud máte nainstalován docker:
```bash
@@ -228,11 +232,11 @@ Twenty vyžaduje ke svému provozu Redis cache pro zajištění nejlepšího vý
</Tab>
</Tabs>
Pokud potřebujete GUI klienta, doporučujeme [redis insight](https://redis.io/insight/) (dostupná je bezplatná verze)
Pokud potřebujete GUI klienta, doporučujeme [Redis Insight](https://redis.io/insight/) (k dispozici je bezplatná verze).
## Krok 5: Nastavení proměnných prostředí
Použijte proměnné prostředí nebo `.env` soubory, abyste nakonfigurovali váš projekt. Více informací [zde](/l/cs/developers/self-host/capabilities/setup)
Použijte proměnné prostředí nebo `.env` soubory, abyste nakonfigurovali váš projekt. Více informací [zde](/l/cs/developers/self-host/capabilities/setup).
Zkopírujte soubory `.env.example` v `/front` a `/server`:
@@ -0,0 +1,147 @@
---
title: API
description: Programově dotazujte a upravujte svá CRM data pomocí REST nebo GraphQL.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
Twenty bylo vytvořeno s ohledem na vývojáře a nabízí výkonná API, která se přizpůsobí vašemu vlastnímu datovému modelu. Nabízíme čtyři typy API, které splňují různé potřeby integrace.
## Přístup orientovaný na vývojáře
Twenty generuje API specificky pro váš datový model:
* **Nejsou vyžadována dlouhá ID**: Používejte v koncových bodech přímo názvy objektů a polí.
* **Standardní a vlastní objekty jsou rovnocenně zpracovány**: Vaše vlastní objekty mají stejnou podporu API jako vestavěné.
* **Vyhrazené koncové body**: Každý objekt a každé pole má svůj vlastní koncový bod API.
* **Vlastní dokumentace**: Generována specificky pro datový model vašeho pracovního prostoru.
<Note>
Vaše personalizovaná dokumentace k API je dostupná v **Nastavení → API & Webhooks** po vytvoření API klíče. Protože Twenty generuje API odpovídající vašemu vlastnímu datovému modelu, dokumentace je jedinečná pro váš pracovní prostor.
</Note>
## Dva typy API
### Core API
Přístupné na `/rest/` nebo `/graphql/`
Pracujte se svými skutečnými **záznamy** (daty):
* Vytvářejte, čtěte, aktualizujte a mazejte osoby, společnosti, příležitosti atd.
* Dotazujte a filtrujte data
* Spravujte vztahy mezi záznamy.
### Metadata API
Přístupné na `/rest/metadata/` nebo `/metadata/`
Spravujte svůj **pracovní prostor a datový model**:
* Vytvářejte, upravujte nebo mazejte objekty a pole.
* Konfigurujte nastavení pracovního prostoru.
* Definujte vztahy mezi objekty
## REST vs GraphQL
Jak Core, tak Metadata API jsou k dispozici ve formátech REST a GraphQL:
| Formát | Dostupné operace |
| ----------- | ---------------------------------------------------------------------- |
| **REST** | CRUD, hromadné operace, operace upsert |
| **GraphQL** | Stejné + **hromadné operace upsert**, dotazy na vztahy v jednom volání |
Zvolte podle svých potřeb — oba formáty přistupují ke stejným datům.
## Koncové body API
| Prostředí | Základní URL |
| ------------------- | ------------------------- |
| **Cloud** | `https://api.twenty.com/` |
| **Vlastní hosting** | `https://{your-domain}/` |
## Ověření
Každý požadavek na API vyžaduje klíč API v hlavičce:
```
Authorization: Bearer YOUR_API_KEY
```
### Vytvořit API klíč
1. Přejděte na **Nastavení → APIs & Webhooks**
2. Klikněte na **+ Vytvořit klíč**
3. Nakonfigurujte:
* **Název**: Popisný název pro klíč
* **Datum vypršení platnosti**: Kdy klíč vyprší
4. Klikněte na **Uložit**
5. **Zkopírujte ihned** — klíč se zobrazí pouze jednou
<VimeoEmbed videoId="928786722" title="Vytvoření klíče API" />
<Warning>
Váš klíč API poskytuje přístup k citlivým datům. Nesdílejte ho s nedůvěryhodnými službami. Pokud je kompromitován, okamžitě ho deaktivujte a vygenerujte nový.
</Warning>
### Přiřaďte roli klíči API
Pro vyšší bezpečnost přiřaďte konkrétní roli, abyste omezili přístup:
1. Přejděte na **Nastavení → Role**
2. Klikněte na roli, kterou chcete přiřadit
3. Otevřete záložku **Přiřazení**
4. V části **API Keys** klikněte na **+ Přiřadit ke klíči API**
5. Vyberte klíč API
Klíč zdědí oprávnění této role. Podrobnosti viz [Oprávnění](/l/cs/user-guide/permissions-access/capabilities/permissions).
### Spravovat API klíče
**Znovu vygenerovat**: Nastavení → APIs & Webhooks → Klikněte na klíč → **Znovu vygenerovat**
**Smazat**: Nastavení → APIs & Webhooks → Klikněte na klíč → **Smazat**
## API Playground
Testujte svá API přímo v prohlížeči pomocí našeho vestavěného playgroundu — k dispozici pro **REST** i **GraphQL**.
### Přístup do Playgroundu
1. Přejděte na **Nastavení → APIs & Webhooks**
2. Vytvořte klíč API (povinné)
3. Klikněte na **REST API** nebo **GraphQL API** pro otevření playgroundu
### Co získáte
* **Interaktivní dokumentace**: Generována pro váš specifický datový model
* **Živé testování**: Spouštějte reálná volání API vůči vašemu pracovnímu prostoru
* **Průzkumník schématu**: Procházejte dostupné objekty, pole a vztahy
* **Tvůrce požadavků**: Sestavujte dotazy s automatickým doplňováním
Playground odráží vaše vlastní objekty a pole, takže dokumentace je pro váš pracovní prostor vždy přesná.
## Hromadné operace
REST i GraphQL podporují hromadné operace:
* **Velikost dávky**: Až 60 záznamů na požadavek.
* **Operace**: Vytváření, aktualizace a mazání více záznamů
**Funkce pouze pro GraphQL:**
* **Hromadný upsert**: Vytvoření nebo aktualizace v jednom volání
* Používejte množná čísla názvů objektů (např. `CreateCompanies` místo `CreateCompany`)
## Limity rychlosti
Požadavky na API jsou omezovány, aby byla zajištěna stabilita platformy:
| Limit | Hodnota |
| ------------------ | -------------------- |
| **Požadavky** | 100 volání za minutu |
| **Velikost dávky** | 60 záznamů na volání |
<Tip>
Pro maximalizaci propustnosti používejte hromadné operace — zpracujte až 60 záznamů v jediném volání API místo odesílání jednotlivých požadavků.
</Tip>
@@ -0,0 +1,689 @@
---
title: Vytváření aplikací
description: Definujte objekty, logické funkce, frontendové komponenty a další pomocí Twenty SDK.
---
<Warning>
Aplikace jsou aktuálně v alfa testování. Tato funkce je funkční, ale stále se vyvíjí.
</Warning>
## Používejte zdroje SDK (typy a konfiguraci)
twenty-sdk poskytuje typované stavební bloky a pomocné funkce, které používáte ve své aplikaci. Níže jsou klíčové části, se kterými budete nejčastěji pracovat.
### Pomocné funkce
SDK poskytuje pomocné funkce pro definování entit vaší aplikace. Jak je popsáno v [Detekce entit](/l/cs/developers/extend/apps/getting-started#entity-detection), musíte použít `export default define<Entity>({...})`, aby byly vaše entity detekovány:
| Funkce | Účel |
| -------------------------------- | ----------------------------------------------------------------- |
| `defineApplication` | Nakonfigurujte metadata aplikace (povinné, jedno na aplikaci) |
| `defineObject` | Definice vlastních objektů s poli |
| `defineLogicFunction` | Definice logických funkcí s obslužnými funkcemi |
| `definePreInstallLogicFunction` | Definujte předinstalační logickou funkci (jedna na aplikaci) |
| `definePostInstallLogicFunction` | Definujte postinstalační logickou funkci (jedna na aplikaci) |
| `defineFrontComponent` | Definujte frontendové komponenty pro vlastní uživatelské rozhraní |
| `defineRole` | Konfigurace oprávnění rolí a přístupu k objektům |
| `defineField` | Rozšiřte existující objekty o další pole |
| `defineView` | Definujte uložená zobrazení pro objekty |
| `defineNavigationMenuItem` | Definujte odkazy postranní navigace |
| `defineSkill` | Definujte dovednosti agenta AI |
Tyto funkce validují vaši konfiguraci v době sestavení a poskytují automatické doplňování v IDE a typovou bezpečnost.
### Definování objektů
Vlastní objekty popisují jak schéma, tak chování záznamů ve vašem pracovním prostoru. K definování objektů s vestavěnou validací použijte `defineObject()`:
```typescript
// src/app/postCard.object.ts
import { defineObject, FieldType } from 'twenty-sdk';
enum PostCardStatus {
DRAFT = 'DRAFT',
SENT = 'SENT',
DELIVERED = 'DELIVERED',
RETURNED = 'RETURNED',
}
export default defineObject({
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
nameSingular: 'postCard',
namePlural: 'postCards',
labelSingular: 'Post Card',
labelPlural: 'Post Cards',
description: 'A post card object',
icon: 'IconMail',
fields: [
{
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
name: 'content',
type: FieldType.TEXT,
label: 'Content',
description: "Postcard's content",
icon: 'IconAbc',
},
{
universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
name: 'recipientName',
type: FieldType.FULL_NAME,
label: 'Recipient name',
icon: 'IconUser',
},
{
universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
name: 'recipientAddress',
type: FieldType.ADDRESS,
label: 'Recipient address',
icon: 'IconHome',
},
{
universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
name: 'status',
type: FieldType.SELECT,
label: 'Status',
icon: 'IconSend',
defaultValue: `'${PostCardStatus.DRAFT}'`,
options: [
{ value: PostCardStatus.DRAFT, label: 'Draft', position: 0, color: 'gray' },
{ value: PostCardStatus.SENT, label: 'Sent', position: 1, color: 'orange' },
{ value: PostCardStatus.DELIVERED, label: 'Delivered', position: 2, color: 'green' },
{ value: PostCardStatus.RETURNED, label: 'Returned', position: 3, color: 'orange' },
],
},
{
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
name: 'deliveredAt',
type: FieldType.DATE_TIME,
label: 'Delivered at',
icon: 'IconCheck',
isNullable: true,
defaultValue: null,
},
],
});
```
Hlavní body:
* Použijte `defineObject()` pro vestavěnou validaci a lepší podporu v IDE.
* Hodnota `universalIdentifier` musí být jedinečná a stabilní napříč nasazeními.
* Každé pole vyžaduje `name`, `type`, `label` a svůj vlastní stabilní `universalIdentifier`.
* Pole `fields` je volitelné — objekty můžete definovat i bez vlastních polí.
* Nové objekty můžete vygenerovat pomocí `yarn twenty entity:add`, který vás provede pojmenováním, poli a vztahy.
<Note>
**Základní pole jsou vytvořena automaticky.** Když definujete vlastní objekt, Twenty automaticky přidá standardní pole
jako `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` a `deletedAt`.
Nemusíte je definovat v poli `fields` — přidejte pouze svá vlastní pole.
Výchozí pole můžete přepsat definováním pole se stejným názvem v poli `fields`,
ale to se nedoporučuje.
</Note>
### Konfigurace aplikace (application-config.ts)
Každá aplikace má jeden soubor `application-config.ts`, který popisuje:
* **Identitu aplikace**: identifikátory, zobrazovaný název a popis.
* **Jak běží její funkce**: kterou roli používají pro oprávnění.
* **(Volitelné) proměnné**: dvojice klíč–hodnota zpřístupněné vašim funkcím jako proměnné prostředí.
* **(Volitelná) předinstalační funkce**: logická funkce, která se spouští před instalací aplikace.
* **(Volitelná) postinstalační funkce**: logická funkce, která se spouští po instalaci aplikace.
K definování konfigurace aplikace použijte `defineApplication()`:
```typescript
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
displayName: 'My Twenty App',
description: 'My first Twenty app',
icon: 'IconWorld',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
description: 'Default recipient name for postcards',
value: 'Jane Doe',
isSecret: false,
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});
```
Poznámky:
* Pole `universalIdentifier` jsou deterministická ID, která vlastníte; vygenerujte je jednou a udržujte je stabilní napříč synchronizacemi.
* `applicationVariables` se stanou proměnnými prostředí pro vaše funkce (například `DEFAULT_RECIPIENT_NAME` je dostupné jako `process.env.DEFAULT_RECIPIENT_NAME`).
* `defaultRoleUniversalIdentifier` se musí shodovat se souborem role (viz níže).
* Předinstalační a postinstalační funkce jsou při sestavování manifestu automaticky detekovány. Viz [Předinstalační funkce](#pre-install-functions) a [Postinstalační funkce](#post-install-functions).
#### Role a oprávnění
Aplikace mohou definovat role, které zapouzdřují oprávnění k objektům a akcím ve vašem pracovním prostoru. Pole `defaultRoleUniversalIdentifier` v `application-config.ts` určuje výchozí roli používanou logickými funkcemi vaší aplikace.
* Běhový klíč API vložený jako `TWENTY_API_KEY` je odvozen z této výchozí role funkcí.
* Typovaný klient bude omezen oprávněními udělenými této roli.
* Dodržujte princip nejmenších oprávnění: vytvořte vyhrazenou roli pouze s oprávněními, která vaše funkce potřebují, a poté odkazujte na její univerzální identifikátor.
##### Výchozí role funkce (*.role.ts)
Když vygenerujete novou aplikaci, CLI také vytvoří výchozí soubor role. K definování rolí s vestavěnou validací použijte `defineRole()`:
```typescript
// src/roles/default-role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
export default defineRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
},
],
fieldPermissions: [
{
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
canReadFieldValue: false,
canUpdateFieldValue: false,
},
],
permissionFlags: [PermissionFlag.APPLICATIONS],
});
```
Na `universalIdentifier` této role se poté odkazuje v `application-config.ts` jako na `defaultRoleUniversalIdentifier`. Jinými slovy:
* **\*.role.ts** definuje, co může výchozí role funkce dělat.
* **application-config.ts** ukazuje na tuto roli, aby vaše funkce zdědily její oprávnění.
Poznámky:
* Začněte rolí vytvořenou scaffolderem a postupně ji omezujte podle principu nejmenších oprávnění.
* Nahraďte `objectPermissions` a `fieldPermissions` objekty/poli, která vaše funkce potřebují.
* `permissionFlags` řídí přístup k schopnostem na úrovni platformy. Držte je na minimu; přidávejte pouze to, co potřebujete.
* Podívejte se na funkční příklad v aplikaci Hello World: [`packages/twenty-apps/hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts).
### Konfigurace logických funkcí a vstupní bod
Každý soubor funkce používá `defineLogicFunction()` k exportu konfigurace s obslužnou funkcí (handlerem) a volitelnými spouštěči.
```typescript
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import { CoreApiClient, type Person } from 'twenty-sdk/generated';
const handler = async (params: RoutePayload) => {
const client = new CoreApiClient();
const name = 'name' in params.queryStringParameters
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: 'Hello world';
const result = await client.mutation({
createPostCard: {
__args: { data: { name } },
id: true,
name: true,
},
});
return result;
};
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'create-new-post-card',
timeoutSeconds: 2,
handler,
triggers: [
// Public HTTP route trigger '/s/post-card/create'
{
universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
type: 'route',
path: '/post-card/create',
httpMethod: 'GET',
isAuthRequired: false,
},
// Cron trigger (CRON pattern)
// {
// universalIdentifier: 'dd802808-0695-49e1-98c9-d5c9e2704ce2',
// type: 'cron',
// pattern: '0 0 1 1 *',
// },
// Database event trigger
// {
// universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156',
// type: 'databaseEvent',
// eventName: 'person.updated',
// updatedFields: ['name'],
// },
],
});
```
Běžné typy spouštěčů:
* **route**: Zpřístupní vaši funkci na HTTP cestě a metodě **pod koncovým bodem `/s/`**:
> např. `path: '/post-card/create',` -> volání na `<APP_URL>/s/post-card/create`
* **cron**: Spouští vaši funkci podle plánu pomocí výrazu CRON.
* **databaseEvent**: Spouští se při událostech životního cyklu objektů v pracovním prostoru. Když je operace události `updated`, lze konkrétní sledovaná pole určit v poli `updatedFields`. Pokud zůstane nedefinované nebo prázdné, spustí funkci jakákoli aktualizace.
> např. `person.updated`
Poznámky:
* Pole `triggers` je volitelné. Funkce bez spouštěčů lze použít jako pomocné funkce volané jinými funkcemi.
* V jedné funkci můžete kombinovat více typů spouštěčů.
### Předinstalační funkce
Předinstalační funkce je logická funkce, která se automaticky spouští před instalací vaší aplikace v pracovním prostoru. To je užitečné pro validační úlohy, kontrolu předpokladů nebo přípravu stavu pracovního prostoru před zahájením hlavní instalace.
Když vygenerujete kostru nové aplikace pomocí `create-twenty-app`, vytvoří se pro vás předinstalační funkce v `src/logic-functions/pre-install.ts`:
```typescript
// src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: '<generated-uuid>',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
handler,
});
```
Předinstalační funkci můžete také kdykoli spustit ručně pomocí CLI:
```bash filename="Terminal"
yarn twenty function:execute --preInstall
```
Hlavní body:
* Předinstalační funkce používají `definePreInstallLogicFunction()` — specializovanou variantu, která vynechává nastavení spouštěčů (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
* Obslužná funkce (handler) obdrží `InstallLogicFunctionPayload` s `{ previousVersion: string }` — verzi aplikace, která byla dříve nainstalována (nebo prázdný řetězec při čisté instalaci).
* Na jednu aplikaci je povolena pouze jedna předinstalační funkce. Sestavení manifestu skončí chybou, pokud je zjištěna více než jedna.
* Identifikátor `universalIdentifier` funkce se během sestavení automaticky nastaví v manifestu aplikace jako `preInstallLogicFunctionUniversalIdentifier` — není potřeba jej uvádět v `defineApplication()`.
* Výchozí časový limit je nastaven na 300 sekund (5 minut), aby umožnil delší přípravné úlohy.
* Předinstalační funkce nepotřebují spouštěče — platforma je vyvolává před instalací nebo je lze spustit ručně pomocí `function:execute --preInstall`.
### Postinstalační funkce
Postinstalační funkce je logická funkce, která se automaticky spouští po instalaci vaší aplikace do pracovního prostoru. To je užitečné pro jednorázové úlohy nastavení, jako je naplnění výchozími daty, vytvoření počátečních záznamů nebo konfigurace nastavení pracovního prostoru.
Když vygenerujete kostru nové aplikace pomocí `create-twenty-app`, vytvoří se pro vás postinstalační funkce v `src/logic-functions/post-install.ts`:
```typescript
// src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
export default definePostInstallLogicFunction({
universalIdentifier: '<generated-uuid>',
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
handler,
});
```
Postinstalační funkci můžete také kdykoli spustit ručně pomocí CLI:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
```
Hlavní body:
* Postinstalační funkce používají `definePostInstallLogicFunction()` — specializovanou variantu, která vynechává nastavení spouštěčů (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
* Obslužná funkce (handler) obdrží `InstallLogicFunctionPayload` s `{ previousVersion: string }` — verzi aplikace, která byla dříve nainstalována (nebo prázdný řetězec při čisté instalaci).
* Na jednu aplikaci je povolena pouze jedna postinstalační funkce. Sestavení manifestu skončí chybou, pokud je zjištěna více než jedna.
* Identifikátor `universalIdentifier` funkce se během sestavení automaticky nastaví v manifestu aplikace jako `postInstallLogicFunctionUniversalIdentifier` — není potřeba jej uvádět v `defineApplication()`.
* Výchozí časový limit je nastaven na 300 sekund (5 minut), aby umožnil delší úlohy nastavení, jako je naplnění daty.
* Postinstalační funkce nepotřebují spouštěče — jsou spouštěny platformou během instalace nebo ručně pomocí `function:execute --postInstall`.
### Payload spouštěče trasy
<Warning>
**Zpětně nekompatibilní změna (v1.16, leden 2026):** Formát payloadu spouštěče trasy se změnil. Před verzí v1.16 byly parametry dotazu, parametry cesty a tělo odesílány přímo jako payload. Od verze v1.16 jsou zanořeny uvnitř strukturovaného objektu `RoutePayload`.
**Před v1.16:**
```typescript
const handler = async (params) => {
const { param1, param2 } = params; // Direct access
};
```
**Po v1.16:**
```typescript
const handler = async (event: RoutePayload) => {
const { param1, param2 } = event.body; // Access via .body
const { queryParam } = event.queryStringParameters;
const { id } = event.pathParameters;
};
```
**Jak migrovat existující funkce:** Aktualizujte svůj handler tak, aby destrukturoval z `event.body`, `event.queryStringParameters` nebo `event.pathParameters` místo přímo z objektu params.
</Warning>
Když spouštěč trasy vyvolá vaši logickou funkci, ta obdrží objekt `RoutePayload`, který odpovídá formátu AWS HTTP API v2. Importujte typ z `twenty-sdk`:
```typescript
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
const handler = async (event: RoutePayload) => {
// Access request data
const { headers, queryStringParameters, pathParameters, body } = event;
// HTTP method and path are available in requestContext
const { method, path } = event.requestContext.http;
return { message: 'Success' };
};
```
Typ `RoutePayload` má následující strukturu:
| Vlastnost | Typ | Popis |
| ---------------------------- | ------------------------------------- | --------------------------------------------------------------------------------- |
| `headers` | `Record<string, string \| undefined>` | Záhlaví HTTP (pouze ta uvedená v `forwardedRequestHeaders`) |
| `queryStringParameters` | `Record<string, string \| undefined>` | Parametry query stringu (více hodnot spojených čárkami) |
| `pathParameters` | `Record<string, string \| undefined>` | Parametry cesty extrahované ze vzoru trasy (např. `/users/:id` → `{ id: '123' }`) |
| `body` | `object \| null` | Parsované tělo požadavku (JSON) |
| `isBase64Encoded` | `boolean` | Zda je tělo kódováno base64 |
| `requestContext.http.method` | `string` | Metoda HTTP (GET, POST, PUT, PATCH, DELETE) |
| `requestContext.http.path` | `string` | Nezpracovaná cesta požadavku |
### Přeposílání záhlaví HTTP
Ve výchozím nastavení se záhlaví HTTP z příchozích požadavků z bezpečnostních důvodů do vaší logické funkce **ne** předávají. Chcete-li zpřístupnit konkrétní záhlaví, výslovně je uveďte v poli `forwardedRequestHeaders`:
```typescript
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'webhook-handler',
handler,
triggers: [
{
universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
type: 'route',
path: '/webhook',
httpMethod: 'POST',
isAuthRequired: false,
forwardedRequestHeaders: ['x-webhook-signature', 'content-type'],
},
],
});
```
Ve vašem handleru k nim poté můžete přistupovat:
```typescript
const handler = async (event: RoutePayload) => {
const signature = event.headers['x-webhook-signature'];
const contentType = event.headers['content-type'];
// Validate webhook signature...
return { received: true };
};
```
<Note>
Názvy záhlaví jsou normalizovány na malá písmena. Přistupujte k nim pomocí klíčů s malými písmeny (například `event.headers['content-type']`).
</Note>
Nové funkce můžete vytvářet dvěma způsoby:
* **Vygenerované**: Spusťte `yarn twenty entity:add` a zvolte možnost přidat novou logickou funkci. Tím se vygeneruje startovací soubor s obslužnou funkcí a konfigurací.
* **Ruční**: Vytvořte nový soubor `*.logic-function.ts` a použijte `defineLogicFunction()` podle stejného vzoru.
### Označení logické funkce jako nástroje
Logické funkce lze zpřístupnit jako **nástroje** pro agenty AI a pracovní postupy. Když je funkce označena jako nástroj, stane se dohledatelnou funkcemi AI produktu Twenty a lze ji vybrat jako krok v automatizacích pracovních postupů.
Chcete-li označit logickou funkci jako nástroj, nastavte `isTool: true` a poskytněte `toolInputSchema` popisující očekávané vstupní parametry pomocí [JSON Schema](https://json-schema.org/):
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-sdk/generated';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
__args: {
data: {
title: `Enrich data for ${params.companyName}`,
body: `Domain: ${params.domain ?? 'unknown'}`,
},
},
id: true,
},
});
return { taskId: result.createTask.id };
};
export default defineLogicFunction({
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
name: 'enrich-company',
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
isTool: true,
toolInputSchema: {
type: 'object',
properties: {
companyName: {
type: 'string',
description: 'The name of the company to enrich',
},
domain: {
type: 'string',
description: 'The company website domain (optional)',
},
},
required: ['companyName'],
},
});
```
Hlavní body:
* **`isTool`** (`boolean`, výchozí: `false`): Když je nastaveno na `true`, funkce je zaregistrována jako nástroj a zpřístupní se agentům AI a automatizacím pracovních postupů.
* **`toolInputSchema`** (`object`, volitelné): Objekt JSON Schema, který popisuje parametry, jež vaše funkce přijímá. Agenti AI používají toto schéma k pochopení toho, jaké vstupy nástroj očekává, a k ověřování volání. Pokud je vynecháno, schéma má výchozí podobu `{ type: 'object', properties: {} }` (žádné parametry).
* Funkce s `isTool: false` (nebo není nastaveno) **nejsou** zpřístupněny jako nástroje. Stále je lze spouštět přímo nebo volat z jiných funkcí, ale neobjeví se ve vyhledávání nástrojů.
* **Pojmenování nástrojů**: Když je funkce zpřístupněna jako nástroj, její název se automaticky normalizuje na `logic_function_<name>` (převedeno na malá písmena, nealfanumerické znaky jsou nahrazeny podtržítky). Například `enrich-company` se změní na `logic_function_enrich_company`.
* Můžete kombinovat `isTool` se spouštěči — funkce může být zároveň nástrojem (volatelným agenty AI) i spouštěna událostmi (cron, databázové události, routes).
<Note>
**Napište kvalitní `description`.** Agenti AI se spoléhají na pole funkce `description` při rozhodování, kdy nástroj použít. Buďte konkrétní ohledně toho, co nástroj dělá a kdy se má volat.
</Note>
### Frontendové komponenty
Frontendové komponenty vám umožňují vytvářet vlastní React komponenty, které se vykreslují v rozhraní Twenty. K definování komponent s vestavěnou validací použijte `defineFrontComponent()`:
```typescript
// src/front-components/my-widget.tsx
import { defineFrontComponent } from 'twenty-sdk';
const MyWidget = () => {
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h1>My Custom Widget</h1>
<p>This is a custom front component for Twenty.</p>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'my-widget',
description: 'A custom widget component',
component: MyWidget,
});
```
Hlavní body:
* Frontendové komponenty jsou React komponenty, které se vykreslují v izolovaných kontextech v rámci Twenty.
* Pole `component` odkazuje na vaši React komponentu.
* Komponenty se během `yarn twenty dev` automaticky sestaví a synchronizují.
Nové frontendové komponenty můžete vytvořit dvěma způsoby:
* **Vygenerované**: Spusťte `yarn twenty entity:add` a zvolte možnost přidat novou frontendovou komponentu.
* **Ruční**: Vytvořte nový soubor `.tsx` a použijte `defineFrontComponent()`, podle stejného vzoru.
### Dovednosti
Dovednosti definují znovupoužitelné pokyny a schopnosti, které mohou agenti AI používat ve vašem pracovním prostoru. K definování dovedností s vestavěnou validací použijte `defineSkill()`:
```typescript
// src/skills/example-skill.ts
import { defineSkill } from 'twenty-sdk';
export default defineSkill({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'sales-outreach',
label: 'Sales Outreach',
description: 'Guides the AI agent through a structured sales outreach process',
icon: 'IconBrain',
content: `You are a sales outreach assistant. When reaching out to a prospect:
1. Research the company and recent news
2. Identify the prospect's role and likely pain points
3. Draft a personalized message referencing specific details
4. Keep the tone professional but conversational`,
});
```
Hlavní body:
* `name` je jedinečný identifikátor dovednosti (doporučuje se kebab-case).
* `label` je uživatelsky čitelný název zobrazovaný v UI.
* `content` obsahuje pokyny dovednosti — je to text, který agent AI používá.
* `icon` (volitelné) nastavuje ikonu zobrazovanou v UI.
* `description` (volitelné) poskytuje doplňující kontext o účelu dovednosti.
Nové dovednosti můžete vytvářet dvěma způsoby:
* **Vygenerované**: Spusťte `yarn twenty entity:add` a zvolte možnost přidat novou dovednost.
* **Ruční**: Vytvořte nový soubor a použijte `defineSkill()` podle stejného vzoru.
### Generované typované klienty
Dva typovaní klienti jsou automaticky generováni pomocí `yarn twenty dev` a ukládají se do `node_modules/twenty-sdk/generated` podle schématu vašeho pracovního prostoru:
* **`CoreApiClient`** — provádí dotazy na endpoint `/graphql` za účelem získání dat pracovního prostoru
* **`MetadataApiClient`** — odesílá dotazy na endpoint `/metadata` pro konfiguraci pracovního prostoru a nahrávání souborů
```typescript
import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/generated';
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
```
Oba klienti se automaticky znovu generují pomocí `yarn twenty dev` kdykoli se změní vaše objekty nebo pole.
#### Běhové přihlašovací údaje v logických funkcích
Když vaše funkce běží na Twenty, platforma před spuštěním kódu vloží přihlašovací údaje jako proměnné prostředí:
* `TWENTY_API_URL`: Základní URL Twenty API, na které vaše aplikace cílí.
* `TWENTY_API_KEY`: Krátkodobý klíč s rozsahem omezeným na výchozí roli funkce vaší aplikace.
Poznámky:
* Není nutné předávat URL ani klíč API vygenerovanému klientovi. Za běhu čte `TWENTY_API_URL` a `TWENTY_API_KEY` z process.env.
* Oprávnění klíče API jsou určena rolí odkazovanou ve vašem `application-config.ts` prostřednictvím `defaultRoleUniversalIdentifier`. Toto je výchozí role používaná logickými funkcemi vaší aplikace.
* Aplikace mohou definovat role podle principu nejmenších oprávnění. Udělte pouze oprávnění, která vaše funkce potřebují, a poté nastavte `defaultRoleUniversalIdentifier` na univerzální identifikátor této role.
#### Nahrávání souborů
Vygenerovaný `MetadataApiClient` obsahuje metodu `uploadFile` pro připojování souborů k polím typu souboru u objektů ve vašem pracovním prostoru. Protože standardní klienti GraphQL nativně nepodporují nahrávání souborů pomocí multipart, klient poskytuje tuto speciální metodu, která interně implementuje [specifikaci multipart požadavků GraphQL](https://github.com/jaydenseric/graphql-multipart-request-spec).
```typescript
import { MetadataApiClient } from 'twenty-sdk/generated';
import * as fs from 'fs';
const metadataClient = new MetadataApiClient();
const fileBuffer = fs.readFileSync('./invoice.pdf');
const uploadedFile = await metadataClient.uploadFile(
fileBuffer, // file contents as a Buffer
'invoice.pdf', // filename
'application/pdf', // MIME type (defaults to 'application/octet-stream')
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universal identifier
);
console.log(uploadedFile);
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
```
Signatura metody:
```typescript
uploadFile(
fileBuffer: Buffer,
filename: string,
contentType: string,
fieldMetadataUniversalIdentifier: string,
): Promise<{ id: string; path: string; size: number; createdAt: string; url: string }>
```
| Parametr | Typ | Popis |
| ---------------------------------- | -------- | --------------------------------------------------------------------------- |
| `fileBuffer` | `Buffer` | Surový obsah souboru |
| `filename` | `string` | Název souboru (používá se pro ukládání a zobrazení) |
| `contentType` | `string` | Typ MIME souboru (pokud je vynechán, výchozí je `application/octet-stream`) |
| `fieldMetadataUniversalIdentifier` | `string` | `universalIdentifier` pole typu souboru ve vašem objektu |
Hlavní body:
* Metoda `uploadFile` je k dispozici v `MetadataApiClient`, protože mutaci nahrávání obsluhuje endpoint `/metadata`.
* Používá `universalIdentifier` pole (nikoli jeho ID specifické pro pracovní prostor), takže váš kód pro nahrávání funguje ve všech pracovních prostorech, kde je vaše aplikace nainstalována — v souladu s tím, jak aplikace odkazují na pole všude jinde.
* Vrácená hodnota `url` je podepsaná adresa URL, kterou můžete použít k přístupu k nahranému souboru.
### Příklad Hello World
Prozkoumejte minimalistický end-to-end příklad, který demonstruje objekty, logické funkce, frontendové komponenty a více spouštěčů [zde](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world).
@@ -0,0 +1,245 @@
---
title: Začínáme
description: Vytvořte svou první aplikaci Twenty během několika minut.
---
<Warning>
Aplikace jsou aktuálně v alfa testování. Tato funkce je funkční, ale stále se vyvíjí.
</Warning>
Aplikace vám umožňují rozšířit Twenty o vlastní objekty, pole, logické funkce, AI schopnosti a komponenty uživatelského rozhraní — vše je spravováno jako kód.
**Co můžete dělat už dnes:**
* Definujte vlastní objekty a pole jako kód (spravovaný datový model)
* Vytvářejte logické funkce s vlastními spouštěči (HTTP routy, cron, databázové události)
* Definujte dovednosti agentů AI
* Vytvářejte frontendové komponenty, které se vykreslují uvnitř uživatelského rozhraní Twenty
* Nasazujte stejnou aplikaci do více pracovních prostorů
## Předpoklady
* Node.js 24+ a Yarn 4
* Docker (pro místní vývojový server Twenty)
## Začínáme
Vytvořte novou aplikaci pomocí oficiálního scaffolderu, poté se ověřte a začněte vyvíjet:
```bash filename="Terminal"
# Scaffold a new app (includes all examples by default)
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Start dev mode: automatically syncs local changes to your workspace
yarn twenty dev
```
Nástroj pro generování kostry podporuje dva režimy pro řízení toho, které ukázkové soubory jsou zahrnuty:
```bash filename="Terminal"
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent)
npx create-twenty-app@latest my-app
# Minimal: only core files (application-config.ts and default-role.ts)
npx create-twenty-app@latest my-app --minimal
```
Odtud můžete:
```bash filename="Terminal"
# Add a new entity to your application (guided)
yarn twenty entity:add
# Watch your application's function logs
yarn twenty function:logs
# Execute a function by name
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Execute the pre-install function
yarn twenty function:execute --preInstall
# Execute the post-install function
yarn twenty function:execute --postInstall
# Uninstall the application from the current workspace
yarn twenty uninstall
# Display commands' help
yarn twenty help
```
Viz také: referenční stránky CLI pro [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) a [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).
## Struktura projektu (vytvořená scaffolderem)
Když spustíte `npx create-twenty-app@latest my-twenty-app`, scaffolder:
* Zkopíruje minimální základní aplikaci do `my-twenty-app/`
* Přidá lokální závislost `twenty-sdk` a konfiguraci pro Yarn 4
* Vytvoří konfigurační soubory a skripty napojené na `twenty` CLI
* Vygeneruje základní soubory (konfigurace aplikace, výchozí role funkcí, předinstalační a postinstalační funkce) a k nim ukázkové soubory podle zvoleného režimu generování kostry
Čerstvě vygenerovaná aplikace s výchozím režimem `--exhaustive` vypadá takto:
```text filename="my-twenty-app/"
my-twenty-app/
package.json
yarn.lock
.gitignore
.nvmrc
.yarnrc.yml
.yarn/
install-state.gz
.oxlintrc.json
tsconfig.json
README.md
public/ # Public assets folder (images, fonts, etc.)
src/
├── application-config.ts # Required - main application configuration
├── roles/
│ └── default-role.ts # Default role for logic functions
├── objects/
│ └── example-object.ts # Example custom object definition
├── fields/
│ └── example-field.ts # Example standalone field definition
├── logic-functions/
│ ├── hello-world.ts # Example logic function
│ ├── pre-install.ts # Pre-install logic function
│ └── post-install.ts # Post-install logic function
├── front-components/
│ └── hello-world.tsx # Example front component
├── views/
│ └── example-view.ts # Example saved view definition
├── navigation-menu-items/
│ └── example-navigation-menu-item.ts # Example sidebar navigation link
└── skills/
└── example-skill.ts # Example AI agent skill definition
```
S volbou `--minimal` se vytvoří pouze základní soubory (`application-config.ts`, `roles/default-role.ts`, `logic-functions/pre-install.ts` a `logic-functions/post-install.ts`).
V kostce:
* **package.json**: Deklaruje název aplikace, verzi, engines (Node 24+, Yarn 4) a přidává `twenty-sdk` plus skript `twenty`, který deleguje na lokální `twenty` CLI. Spusťte `yarn twenty help` pro výpis všech dostupných příkazů.
* **.gitignore**: Ignoruje běžné artefakty jako `node_modules`, `.yarn`, `generated/` (typovaný klient), `dist/`, `build/`, složky s coverage, logy a soubory `.env*`.
* **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Zamykají a konfigurují nástrojový řetězec Yarn 4 používaný projektem.
* **.nvmrc**: Fixuje verzi Node.js požadovanou projektem.
* **.oxlintrc.json** a **tsconfig.json**: Poskytují lintování a konfiguraci TypeScriptu pro zdrojové soubory vaší aplikace v TypeScriptu.
* **README.md**: Krátké README v kořeni aplikace se základními pokyny.
* **public/**: Složka pro ukládání veřejných prostředků (obrázky, písma, statické soubory), které bude vaše aplikace poskytovat. Soubory umístěné zde se během synchronizace nahrají a jsou za běhu dostupné.
* **src/**: Hlavní místo, kde definujete svou aplikaci jako kód
### Detekce entit
SDK detekuje entity analýzou vašich souborů TypeScript a hledá volání **`export default define<Entity>({...})`**. Každý typ entity má odpovídající pomocnou funkci exportovanou z `twenty-sdk`:
| Pomocná funkce | Typ entity |
| -------------------------------- | --------------------------------------------------------- |
| `defineObject` | Definice vlastních objektů |
| `defineLogicFunction` | Definice logických funkcí |
| `definePreInstallLogicFunction` | Předinstalační logická funkce (spouští se před instalací) |
| `definePostInstallLogicFunction` | Postinstalační logická funkce (spouští se po instalaci) |
| `defineFrontComponent` | Definice frontendových komponent |
| `defineRole` | Definice rolí |
| `defineField` | Rozšíření polí u existujících objektů |
| `defineView` | Definice uložených zobrazení |
| `defineNavigationMenuItem` | Definice položek navigační nabídky |
| `defineSkill` | Definice dovedností agenta AI |
<Note>
**Pojmenování souborů je flexibilní.** Detekce entit je založená na AST — SDK prochází vaše zdrojové soubory a hledá vzor `export default define<Entity>({...})`. Soubory a složky můžete organizovat, jak chcete. Seskupování podle typu entity (např. `logic-functions/`, `roles/`) je pouze konvence pro organizaci kódu, nikoli požadavek.
</Note>
Příklad detekované entity:
```typescript
// This file can be named anything and placed anywhere in src/
import { defineObject, FieldType } from 'twenty-sdk';
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCard',
// ... rest of config
});
```
Pozdější příkazy přidají další soubory a složky:
* `yarn twenty dev` will auto-generate two typed API clients in `node_modules/twenty-sdk/generated`: `CoreApiClient` (for workspace data via `/graphql`) and `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`).
* `yarn twenty entity:add` přidá soubory s definicemi entit do `src/` pro vaše vlastní objekty, funkce, frontové komponenty, role, dovednosti a další.
## Ověření
Při prvním spuštění `yarn twenty auth:login` budete vyzváni k zadání:
* URL API (výchozí je http://localhost:3000 nebo váš aktuální profil pracovního prostoru)
* Klíč API
Vaše přihlašovací údaje se ukládají pro jednotlivé uživatele do `~/.twenty/config.json`. Můžete spravovat více profilů a přepínat mezi nimi.
### Správa pracovních prostorů
```bash filename="Terminal"
# Login interactively (recommended)
yarn twenty auth:login
# Login to a specific workspace profile
yarn twenty auth:login --workspace my-custom-workspace
# List all configured workspaces
yarn twenty auth:list
# Switch the default workspace (interactive)
yarn twenty auth:switch
# Switch to a specific workspace
yarn twenty auth:switch production
# Check current authentication status
yarn twenty auth:status
```
Jakmile přepnete pracovní prostor pomocí `yarn twenty auth:switch`, všechny následující příkazy budou tento pracovní prostor používat jako výchozí. Můžete jej stále dočasně přepsat pomocí `--workspace <name>`.
## Ruční nastavení (bez scaffolderu)
Ačkoli pro nejlepší začátky doporučujeme použít `create-twenty-app`, projekt můžete nastavit i ručně. Neinstalujte CLI globálně. Místo toho přidejte `twenty-sdk` jako lokální závislost a přidejte jeden skript do souboru package.json:
```bash filename="Terminal"
yarn add -D twenty-sdk
```
Poté přidejte skript `twenty`:
```json filename="package.json"
{
"scripts": {
"twenty": "twenty"
}
}
```
Now you can run all commands via `yarn twenty <command>`, e.g. `yarn twenty dev`, `yarn twenty help`, etc.
## Jak používat lokální instanci Twenty
Pokud již lokálně provozujete instanci Twenty (např. pomocí `npx nx start twenty-server`), můžete se k ní připojit namísto použití Dockeru:
```bash filename="Terminal"
# During scaffolding — skip Docker, connect to your running instance
npx create-twenty-app@latest my-app --port 3000
# Or after scaffolding — add a remote pointing to your instance
yarn twenty remote add --local --port 3000
```
## Řešení potíží
* Chyby ověření: spusťte `yarn twenty auth:login` a ujistěte se, že váš klíč API má požadovaná oprávnění.
* Nelze se připojit k serveru: ověřte URL API a že je server Twenty dosažitelný.
* Types or client missing/outdated: restart `yarn twenty dev` — it auto-generates the typed client.
* Dev mode not syncing: ensure `yarn twenty dev` is running and that changes are not ignored by your environment.
Kanál podpory na Discordu: https://discord.com/channels/1130383047699738754/1130386664812982322
@@ -0,0 +1,119 @@
---
title: Publikování
description: Distribuujte svou aplikaci Twenty do Marketplace nebo ji nasaďte interně.
---
<Warning>
Aplikace jsou aktuálně v alfa testování. Tato funkce je funkční, ale stále se vyvíjí.
</Warning>
## Přehled
Jakmile je vaše aplikace [sestavena a otestována lokálně](/l/cs/developers/extend/apps/building), máte dvě cesty, jak ji distribuovat:
* **Publish to npm** — uveďte svou aplikaci v Marketplace Twenty, aby ji mohl kterýkoli pracovní prostor objevit a nainstalovat.
* **Odeslat tarball** — nasaďte svou aplikaci na konkrétní server Twenty pro interní použití, aniž by byla veřejně dostupná.
## Publikování na npm
Publikování na npm zajistí, že bude vaše aplikace dohledatelná v Marketplace Twenty. Jakýkoli pracovní prostor Twenty může procházet, instalovat a aktualizovat aplikace z Marketplace přímo z UI.
### Požadavky
* Účet na [npm](https://www.npmjs.com)
* Název vašeho balíčku **musí** používat předponu `twenty-app-` (např. `twenty-app-postcard-sender`)
### Postup
1. **Sestavte svou aplikaci** — CLI zkompiluje vaše zdrojové soubory TypeScript a vygeneruje manifest aplikace:
```bash filename="Terminal"
yarn twenty build
```
2. **Publikujte na npm** — odešlete sestavený balíček do registru npm:
```bash filename="Terminal"
npx twenty publish
```
### Automatické rozpoznání
Balíčky s předponou `twenty-app-` jsou automaticky rozpoznávány katalogem Marketplace Twenty. Po publikování se vaše aplikace během několika minut objeví v Marketplace — nevyžaduje žádnou ruční registraci ani schvalování.
### Publikování pomocí CI
Vygenerovaný projekt obsahuje pracovní postup GitHub Actions, který publikuje při každém vydání. Spouští `app:build`, a poté `npm publish --provenance` z výstupu buildu:
```yaml filename=".github/workflows/publish.yml"
name: Publish
on:
release:
types: [published]
permissions:
contents: read
id-token: write
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "24"
registry-url: https://registry.npmjs.org
- run: yarn install --immutable
- run: npx twenty build
- run: npm publish --provenance --access public
working-directory: .twenty/output
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
For other CI systems (GitLab CI, CircleCI, etc.), the same three commands apply: `yarn install`, `npx twenty build`, then `npm publish` from `.twenty/output`.
<Tip>
**npm provenance** je volitelné, ale doporučené. Publikování s `--provenance` přidá k vašemu záznamu na npm odznak důvěryhodnosti a umožní uživatelům ověřit, že balíček byl sestaven z konkrétního commitu ve veřejné CI pipeline. Pokyny k nastavení najdete v [dokumentaci k npm provenance](https://docs.npmjs.com/generating-provenance-statements).
</Tip>
## Interní distribuce
Pro aplikace, které nechcete zpřístupnit veřejně — proprietární nástroje, integrace pouze pro enterprise nebo experimentální buildy — můžete odeslat tarball přímo na server Twenty.
### Odeslat tarball
Sestavte svou aplikaci a nasaďte ji na konkrétní server v jednom kroku:
```bash filename="Terminal"
npx twenty publish --server <server-url>
```
Jakýkoli pracovní prostor na tomto serveru pak může aplikaci instalovat a aktualizovat ze stránky nastavení **Applications**.
### Správa verzí
Chcete-li vydat aktualizaci:
1. Zvyšte hodnotu pole `version` v souboru `package.json`
2. Push a new tarball with `npx twenty publish --server <server-url>`
3. Pracovní prostory na tomto serveru uvidí dostupnou aktualizaci ve svém nastavení
<Note>
Interní aplikace jsou omezené na server, na který jsou odeslány. Nezobrazí se ve veřejném Marketplace a nelze je instalovat v pracovních prostorech na jiných serverech.
</Note>
## Kategorie aplikací
Twenty organizuje aplikace do tří kategorií podle způsobu distribuce:
| Kategorie | Jak to funguje | Viditelné v Marketplace? |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
| **Vývoj** | Local dev mode apps running via `yarn twenty dev`. Slouží k sestavování a testování. | Ne |
| **Publikováno** | Aplikace publikované na npm s předponou `twenty-app-`. Uvedeny v Marketplace, aby je mohl kterýkoli pracovní prostor nainstalovat. | Ano |
| **Interní** | Aplikace nasazené pomocí tarballu na konkrétní server. Dostupné pouze pro pracovní prostory na tomto serveru. | Ne |
<Tip>
Začněte v režimu **Development** při sestavování své aplikace. Až bude připravena, zvolte **Published** (npm) pro širokou distribuci nebo **Internal** (tarball) pro soukromé nasazení.
</Tip>
@@ -21,19 +21,51 @@ Aplikace vám umožňují vytvářet a spravovat přizpůsobení Twenty **jako k
## Předpoklady
* Node.js 24+ a Yarn 4
* Pracovní prostor Twenty a klíč API (vytvořte si jej na https://app.twenty.com/settings/api-webhooks)
* Docker (pro místní vývojový server Twenty)
## Začínáme
Vytvořte novou aplikaci pomocí oficiálního scaffolderu, poté se ověřte a začněte vyvíjet:
Vytvořte novou aplikaci pomocí oficiálního generátoru kostry. Může vám automaticky spustit místní instanci Twenty:
```bash filename="Terminal"
# Vygenerujte kostru nové aplikace (ve výchozím nastavení zahrnuje všechny příklady)
# Vygenerujte kostru nové aplikace — CLI nabídne spuštění místního serveru Twenty
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Spusťte vývojový režim: automaticky synchronizuje místní změny s vaším pracovním prostorem
yarn twenty app:dev
yarn twenty dev
```
### Správa místního serveru
SDK obsahuje příkazy ke správě místního vývojového serveru Twenty (all-in-one obraz Dockeru s PostgreSQL, Redisem, serverem a workerem):
```bash filename="Terminal"
# Spusťte místní server (v případě potřeby stáhne obraz)
yarn twenty server start
# Zkontrolujte stav serveru
yarn twenty server status
# Streamujte logy serveru
yarn twenty server logs
# Zastavte server
yarn twenty server stop
# Resetujte všechna data a začněte znovu
yarn twenty server reset
```
Lokální server je předem naplněn pracovním prostorem a uživatelem (`tim@apple.dev` / `tim@apple.dev`), takže můžete začít vyvíjet okamžitě bez jakéhokoli ručního nastavení.
### Ověření
Připojte svou aplikaci k lokálnímu serveru pomocí OAuth:
```bash filename="Terminal"
# Ověřte se pomocí OAuth (otevře prohlížeč)
yarn twenty remote add --local
```
Nástroj pro generování kostry podporuje dva režimy pro řízení toho, které ukázkové soubory jsou zahrnuty:
@@ -46,28 +78,46 @@ npx create-twenty-app@latest my-app
npx create-twenty-app@latest my-app --minimal
```
### Jak používat lokální instanci Twenty
Pokud již lokálně provozujete instanci Twenty, můžete se k ní připojit místo použití Dockeru. Zadejte port, na kterém váš lokální server naslouchá (výchozí: `3000`):
```bash filename="Terminal"
# Během vytváření kostry
npx create-twenty-app@latest my-app --port 3000
# Nebo po vytvoření kostry
yarn twenty remote add --local --port 3000
```
Odtud můžete:
```bash filename="Terminal"
# Přidejte do vaší aplikace novou entitu (s průvodcem)
# Add a new entity to your application (guided)
yarn twenty entity:add
# Sledujte logy funkcí vaší aplikace
# Watch your application's function logs
yarn twenty function:logs
# Spusťte funkci podle názvu
# Execute a function by name
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Spusťte předinstalační funkci
# Execute the pre-install function
yarn twenty function:execute --preInstall
# Spusťte postinstalační funkci
# Execute the post-install function
yarn twenty function:execute --postInstall
# Odinstalujte aplikaci z aktuálního pracovního prostoru
yarn twenty app:uninstall
# Build the app for distribution
yarn twenty build
# Zobrazte nápovědu k příkazům
# Publish the app to npm or a Twenty server
yarn twenty publish
# Uninstall the application from the current workspace
yarn twenty uninstall
# Display commands' help
yarn twenty help
```
@@ -171,7 +221,7 @@ export default defineObject({
Pozdější příkazy přidají další soubory a složky:
* `yarn twenty app:dev` will auto-generate two typed API clients in `node_modules/twenty-sdk/clients`: `CoreApiClient` (for workspace data via `/graphql`) and `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`).
* `yarn twenty dev` will auto-generate two typed API clients in `node_modules/twenty-sdk/clients`: `CoreApiClient` (for workspace data via `/graphql`) and `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`).
* `yarn twenty entity:add` přidá soubory s definicemi entit do `src/` pro vaše vlastní objekty, funkce, frontové komponenty, role, dovednosti a další.
## Ověření
@@ -321,6 +371,72 @@ Výchozí pole můžete přepsat definováním pole se stejným názvem v poli `
ale to se nedoporučuje.
</Note>
### Definování polí u existujících objektů
Použijte `defineField()` k přidání vlastních polí k existujícím objektům — jak ke standardním objektům (např. `company`, `person`, `opportunity`), tak k vlastním objektům definovaným jinými aplikacemi. Každé pole je ve svém vlastním souboru a odkazuje na cílový objekt pomocí jeho `universalIdentifier`.
Chcete-li odkazovat na standardní objekty, importujte `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` z `twenty-sdk`. Tato konstanta poskytuje stabilní identifikátory pro všechny vestavěné objekty a jejich pole:
```typescript
// src/fields/apollo-total-funding.field.ts
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
export default defineField({
universalIdentifier: 'c90ae72d-4ddf-4f22-882f-eef98c91e40e',
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.CURRENCY,
name: 'apolloTotalFunding',
label: 'Total Funding',
description: 'Total funding raised by the company',
icon: 'IconCash',
});
```
Hlavní body:
* `objectUniversalIdentifier` určuje, ke kterému objektu má Twenty pole připojit. Použijte `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.<objectName>.universalIdentifier` pro standardní objekty.
* Každé pole vyžaduje svůj vlastní stabilní `universalIdentifier`, `name`, `type`, `label` a cílový `objectUniversalIdentifier`.
* Nová pole můžete vygenerovat pomocí `yarn twenty entity:add` a zvolit možnost pole.
* `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` je pro pohodlí exportován také jako `STANDARD_OBJECT` — obojí odkazuje na stejnou konstantu.
Mezi dostupné standardní objekty patří: `attachment`, `blocklist`, `calendarChannel`, `calendarEvent`, `calendarEventParticipant`, `company`, `connectedAccount`, `dashboard`, `favorite`, `favoriteFolder`, `message`, `messageChannel`, `messageParticipant`, `messageThread`, `note`, `noteTarget`, `opportunity`, `person`, `task`, `taskTarget`, `timelineActivity`, `workflow`, `workflowAutomatedTrigger`, `workflowRun`, `workflowVersion` a `workspaceMember`.
Každý standardní objekt také zpřístupňuje identifikátory svých polí. Například chcete-li odkázat na konkrétní pole u standardního objektu v oprávněních pro role:
```typescript
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier
```
#### Vztahová pole u existujících objektů
Můžete také definovat vztahová pole, která propojí existující objekty s vašimi vlastními objekty:
```typescript
// src/fields/people-on-call-recording.field.ts
import { defineField, FieldType, RelationType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk';
import { CALL_RECORDING_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/objects/call-recording';
import { CALL_RECORDING_ON_PERSON_ID } from 'src/fields/call-recording-on-person.field';
export default defineField({
universalIdentifier: '4a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d',
objectUniversalIdentifier:
CALL_RECORDING_OBJECT_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'person',
label: 'Person',
relationTargetObjectMetadataUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier:
CALL_RECORDING_ON_PERSON_ID,
relationType: RelationType.MANY_TO_ONE,
});
```
### Konfigurace aplikace (application-config.ts)
Každá aplikace má jeden soubor `application-config.ts`, který popisuje:
@@ -764,13 +880,262 @@ Hlavní body:
* Frontendové komponenty jsou React komponenty, které se vykreslují v izolovaných kontextech v rámci Twenty.
* Pole `component` odkazuje na vaši React komponentu.
* Komponenty se během `yarn twenty app:dev` automaticky sestaví a synchronizují.
* Components are built and synced automatically during `yarn twenty dev`.
Nové frontendové komponenty můžete vytvořit dvěma způsoby:
* **Vygenerované**: Spusťte `yarn twenty entity:add` a zvolte možnost přidat novou frontendovou komponentu.
* **Ruční**: Vytvořte nový soubor `.tsx` a použijte `defineFrontComponent()`, podle stejného vzoru.
#### Kde lze použít front komponenty
Front komponenty se mohou vykreslovat na dvou místech v rámci Twenty:
* **Postranní panel** — Ne-headless front komponenty se otevírají v pravém postranním panelu. Toto je výchozí chování, když je front komponenta vyvolána z menu příkazů.
* **Widgety (nástěnky a stránky záznamů)** — Front komponenty lze vkládat jako widgety do rozložení stránek. Při konfiguraci nástěnky nebo rozložení stránky záznamu mohou uživatelé přidat widget front komponenty.
#### Headless vs. ne-headless
Front komponenty existují ve dvou režimech vykreslování řízených volbou `isHeadless`:
**Ne-headless (výchozí)** — Komponenta vykreslí viditelné uživatelské rozhraní. Po vyvolání z menu příkazů se otevře v postranním panelu. Toto je výchozí chování, když je `isHeadless` `false` nebo když tato volba není uvedena.
**Headless** — Komponenta se neviditelně inicializuje na pozadí. Neotevírá postranní panel. Headless komponenty jsou určené pro akce, které provedou logiku a poté se odpojí — například spuštění asynchronního úkolu, navigaci na stránku nebo zobrazení potvrzovacího modálního okna. Přirozeně se hodí ke komponentám SDK Command popsaným níže.
```typescript
export default defineFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'my-action',
description: 'Runs an action without opening the side panel',
component: MyAction,
isHeadless: true,
command: {
universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f12345678901',
label: 'Run my action',
},
});
```
#### Přidávání položek menu příkazů
Aby se front komponenta zobrazila jako položka v menu příkazů Twenty, přidejte vlastnost `command` k `defineFrontComponent()`. Když uživatelé otevřou menu příkazů (Cmd+K / Ctrl+K), položka se zobrazí a po kliknutí spustí front komponentu.
Objekt `command` přijímá následující pole:
| Pole | Typ | Popis |
| --------------------------------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `universalIdentifier` | `string` (povinné) | Jedinečné ID položky menu příkazů |
| `štítek` | `string` (povinné) | Text zobrazený v menu příkazů |
| `ikona` | `string` (nepovinné) | Název ikony (např. `'IconSparkles'`) |
| `isPinned` | `boolean` (nepovinné) | Zda je příkaz připnutý nahoře v menu |
| `availabilityType` | `'GLOBAL' \| 'RECORD_SELECTION'` (nepovinné) | `GLOBAL` zobrazuje příkaz všude; `RECORD_SELECTION` jej zobrazuje pouze v kontextech záznamů |
| `availabilityObjectUniversalIdentifier` | `string` (nepovinné) | Omezí příkaz na konkrétní typ objektu (např. Person) |
Zde je příklad z aplikace pro nahrávání hovorů, který přidává příkaz omezený na záznamy typu Person:
```typescript
import { defineFrontComponent } from 'twenty-sdk';
export default defineFrontComponent({
universalIdentifier: 'c3d4e5f6-a7b8-9012-cdef-123456789012',
name: 'Summarize Person Call Recordings',
description: 'Generates a summary of call recordings for a person',
component: SummarizePersonRecordings,
command: {
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-234567890123',
label: 'Summarize call recordings',
icon: 'IconSparkles',
isPinned: false,
availabilityType: 'RECORD_SELECTION',
availabilityObjectUniversalIdentifier:
'20202020-e674-48e5-a542-72570eee7213',
},
});
```
Když se příkaz synchronizuje, objeví se v menu příkazů. Pokud je front komponenta ne-headless, otevře se postranní panel s komponentou vykreslenou uvnitř. Pokud je headless, komponenta se inicializuje na pozadí a provede svou logiku.
#### Komponenty SDK Command
Balíček `twenty-sdk` poskytuje čtyři pomocné komponenty Command navržené pro headless front komponenty. Každá komponenta při připojení provede akci, chyby zpracuje zobrazením oznámení ve snackbaru a po dokončení automaticky odpojí front komponentu.
Importujte je z `twenty-sdk/command`:
* **`Command`** — Spustí asynchronní callback přes prop `execute`.
* **`CommandLink`** — Naviguje na cestu v aplikaci. Props: `to`, `params`, `queryParams`, `options`.
* **`CommandModal`** — Otevře potvrzovací modální okno. Pokud uživatel potvrdí, provede callback `execute`. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`.
* **`CommandOpenSidePanelPage`** — Otevře konkrétní stránku postranního panelu. Props: `page`, `pageTitle`, `pageIcon`.
Zde je kompletní příklad headless front komponenty, která pomocí `Command` spouští akci z menu příkazů:
```typescript
// src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk';
import { Command } from 'twenty-sdk/command';
import { CoreApiClient } from 'twenty-sdk/clients';
const RunAction = () => {
const execute = async () => {
const client = new CoreApiClient();
await client.mutation({
createTask: {
__args: { data: { title: 'Created by my app' } },
id: true,
},
});
};
return <Command execute={execute} />;
};
export default defineFrontComponent({
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
name: 'run-action',
description: 'Creates a task from the command menu',
component: RunAction,
isHeadless: true,
command: {
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
icon: 'IconPlayerPlay',
},
});
```
A příklad s použitím `CommandModal` k vyžádání potvrzení před provedením:
```typescript
// src/front-components/delete-draft.tsx
import { defineFrontComponent } from 'twenty-sdk';
import { CommandModal } from 'twenty-sdk/command';
const DeleteDraft = () => {
const execute = async () => {
// perform the deletion
};
return (
<CommandModal
title="Delete draft?"
subtitle="This action cannot be undone."
execute={execute}
confirmButtonText="Delete"
confirmButtonAccent="danger"
/>
);
};
export default defineFrontComponent({
universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
name: 'delete-draft',
description: 'Deletes a draft with confirmation',
component: DeleteDraft,
isHeadless: true,
command: {
universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
label: 'Delete draft',
icon: 'IconTrash',
},
});
```
#### Kontext provádění
Každá front komponenta získá kontext provádění, který poskytuje informace o tom, kde a jak běží. K hodnotám kontextu přistupujte pomocí hooků z `twenty-sdk`:
| Hook | Návratový typ | Popis |
| ----------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `useFrontComponentId()` | `string` | Jedinečné ID aktuální instance front komponenty |
| `useRecordId()` | `string \| null` | ID aktuálního záznamu, když komponenta běží v kontextu záznamu (např. widget na stránce záznamu nebo příkaz omezený na záznam). V opačném případě vrací `null`. |
| `useUserId()` | `string \| null` | ID aktuálního uživatele |
```typescript
import { useRecordId, useUserId } from 'twenty-sdk';
const MyWidget = () => {
const recordId = useRecordId();
const userId = useUserId();
return (
<div>
<p>Record: {recordId ?? 'none'}</p>
<p>User: {userId ?? 'anonymous'}</p>
</div>
);
};
```
Kontext je reaktivní — pokud se okolní záznam změní, hooky automaticky vrátí aktualizované hodnoty.
#### Funkce hostitelského API
Front komponenty běží v izolovaném sandboxu, ale mohou interagovat s UI Twenty prostřednictvím sady funkcí poskytovaných hostitelem. Importujte je přímo z `twenty-sdk`:
```typescript
import {
navigate,
closeSidePanel,
enqueueSnackbar,
unmountFrontComponent,
openSidePanelPage,
openCommandConfirmationModal,
} from 'twenty-sdk';
```
| Funkce | Signatura | Popis |
| ------------------------------ | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `navigovat` | `(to, params?, queryParams?, options?) => Promise<void>` | Přejde na typovanou cestu aplikace v rámci Twenty |
| `closeSidePanel` | `() => Promise<void>` | Zavře postranní panel |
| `enqueueSnackbar` | `(params) => Promise<void>` | Zobrazí oznámení ve snackbaru. Parametry: `message`, `variant` (`'error'`, `'success'`, `'info'`, `'warning'`), volitelně `duration`, `detailedMessage`, `dedupeKey` |
| `unmountFrontComponent` | `() => Promise<void>` | Odpojí aktuální front komponentu (používají headless komponenty k úklidu po vykonání) |
| `openSidePanelPage` | `(params) => Promise<void>` | Otevře stránku v postranním panelu. Parametry: `page`, `pageTitle`, `pageIcon`, `shouldResetSearchState` |
| `openCommandConfirmationModal` | `(params) => Promise<'confirm' \| 'cancel'>` | Zobrazí potvrzovací modální okno a počká na reakci uživatele. Parametry: `title`, `subtitle`, `confirmButtonText`, `confirmButtonAccent` (`'default'`, `'blue'`, `'danger'`) |
Zde je příklad, který používá hostitelské API k zobrazení snackbaru a zavření postranního panelu po dokončení akce:
```typescript
import { defineFrontComponent, useRecordId } from 'twenty-sdk';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-sdk/clients';
const ArchiveRecord = () => {
const recordId = useRecordId();
const handleArchive = async () => {
const client = new CoreApiClient();
await client.mutation({
updateTask: {
__args: { id: recordId, data: { status: 'ARCHIVED' } },
id: true,
},
});
await enqueueSnackbar({
message: 'Record archived',
variant: 'success',
});
await closeSidePanel();
};
return (
<div style={{ padding: '20px' }}>
<p>Archive this record?</p>
<button onClick={handleArchive}>Archive</button>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
name: 'archive-record',
description: 'Archives the current record',
component: ArchiveRecord,
});
```
### Dovednosti
Dovednosti definují znovupoužitelné pokyny a schopnosti, které mohou agenti AI používat ve vašem pracovním prostoru. K definování dovedností s vestavěnou validací použijte `defineSkill()`:
@@ -843,7 +1208,7 @@ Nové agenty můžete vytvářet dvěma způsoby:
### Generované typované klienty
Two typed clients are auto-generated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/clients` based on your workspace schema:
Two typed clients are auto-generated by `yarn twenty dev` and stored in `node_modules/twenty-sdk/clients` based on your workspace schema:
* **`CoreApiClient`** — provádí dotazy na endpoint `/graphql` za účelem získání dat pracovního prostoru
* **`MetadataApiClient`** — odesílá dotazy na endpoint `/metadata` pro konfiguraci pracovního prostoru a nahrávání souborů
@@ -859,7 +1224,7 @@ const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
```
`CoreApiClient` is re-generated automatically by `yarn twenty app:dev` whenever your objects or fields change. `MetadataApiClient` ships pre-built with the SDK.
`CoreApiClient` is re-generated automatically by `yarn twenty dev` whenever your objects or fields change. `MetadataApiClient` je v SDK k dispozici již předem sestavený.
#### Běhové přihlašovací údaje v logických funkcích
@@ -876,7 +1241,7 @@ Poznámky:
#### Nahrávání souborů
The `MetadataApiClient` includes an `uploadFile` method for attaching files to file-type fields on your workspace objects. Protože standardní klienti GraphQL nativně nepodporují nahrávání souborů pomocí multipart, klient poskytuje tuto speciální metodu, která interně implementuje [specifikaci multipart požadavků GraphQL](https://github.com/jaydenseric/graphql-multipart-request-spec).
`MetadataApiClient` obsahuje metodu `uploadFile` pro připojování souborů k polím typu souboru u objektů ve vašem pracovním prostoru. Protože standardní klienti GraphQL nativně nepodporují nahrávání souborů pomocí multipart, klient poskytuje tuto speciální metodu, která interně implementuje [specifikaci multipart požadavků GraphQL](https://github.com/jaydenseric/graphql-multipart-request-spec).
```typescript
import { MetadataApiClient } from 'twenty-sdk/clients';
@@ -925,6 +1290,113 @@ Hlavní body:
Prozkoumejte minimalistický end-to-end příklad, který demonstruje objekty, logické funkce, frontendové komponenty a více spouštěčů [zde](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
## Sestavení vaší aplikace
Jakmile vyvinete svou aplikaci pomocí `app:dev`, použijte `app:build` k jejímu zkompilování do distribučního balíčku.
```bash filename="Terminal"
# Build the app (output goes to .twenty/output/)
yarn twenty build
# Build and create a tarball (.tgz) for distribution
yarn twenty build --tarball
```
Proces sestavení:
1. **Parsuje a ověřuje manifest** — čte všechny entity `defineX()` z vašich zdrojových souborů a ověřuje strukturu manifestu.
2. **Kompiluje logické funkce a frontendové komponenty** — slučuje zdrojové soubory TypeScriptu do ESM souborů `.mjs` pomocí esbuild.
3. **Generuje kontrolní součty** — vypočítá MD5 hashe pro každý sestavený soubor, které jsou v manifestu uloženy jako `builtHandlerChecksum` / `builtComponentChecksum`.
4. **Vygeneruje typovaného klienta API** — prozkoumá schéma GraphQL a vygeneruje typované klienty `CoreApiClient` a `MetadataApiClient`.
5. **Spustí kontrolu typů TypeScriptu** — spustí `tsc --noEmit`, aby zachytil chyby typů před publikováním.
6. **Znovu sestaví s vygenerovaným klientem** — provede druhý průchod kompilace, aby byly zahrnuty typy vygenerovaného klienta.
7. **Volitelně vytvoří tarball** — pokud je předán `--tarball`, spustí `npm pack` a vytvoří soubor `.tgz` připravený k distribuci.
Výstup sestavení v `.twenty/output/` obsahuje:
```text
.twenty/output/
├── manifest.json # Manifest with checksums for all built files
├── package.json # Copied from app root
├── yarn.lock # Copied from app root
├── src/
│ ├── logic-functions/ # Compiled .mjs logic function files
│ └── front-components/ # Compiled .mjs front component files
├── public/ # Static assets (if any)
└── my-app-1.0.0.tgz # Only with --tarball flag
```
| Možnost | Popis |
| ----------- | ----------------------------------------------------- |
| `[appPath]` | Cesta k adresáři aplikace (výchozí: aktuální adresář) |
| `--tarball` | Také zabalí výstup do tarballu `.tgz` |
## Publikování vaší aplikace
Použijte `app:publish` k distribuci své aplikace — buď do registru npm, nebo přímo na server Twenty.
### Publikovat na npm (výchozí)
```bash filename="Terminal"
# Publish to npm (requires npm login)
yarn twenty publish
# Publish with a dist-tag (e.g. beta, next)
yarn twenty publish --tag beta
```
Tímto se aplikace sestaví a spustí se `npm publish` z adresáře `.twenty/output/`. Publikovaný balíček pak může být nainstalován z tržiště Twenty jakýmkoli pracovním prostorem.
### Publikovat na server Twenty
```bash filename="Terminal"
# Publish directly to a Twenty server
yarn twenty publish --server https://app.twenty.com
```
Tímto se aplikace sestaví s tarballem, nahraje se na server pomocí GraphQL mutace `uploadAppTarball` a v jednom kroku se spustí instalace. To je užitečné pro soukromá nasazení nebo testování proti konkrétnímu serveru.
| Možnost | Popis |
| ----------------- | ------------------------------------------------------------------ |
| `[appPath]` | Cesta k adresáři aplikace (výchozí: aktuální adresář) |
| `--server <url>` | Publikovat na server Twenty místo npm |
| `--token <token>` | Autentizační token pro cílový server |
| `--tag <tag>` | npm dist-tag (např. `beta`, `next`) — pouze pro publikování na npm |
## Registrace aplikace
Než může být aplikace nainstalována v pracovním prostoru, musí být **zaregistrována**. Registrace je záznam metadat, který popisuje, odkud aplikace pochází a jak ji autentizovat. Ve většině případů to CLI zpracuje automaticky.
### Typy zdrojů
Každá registrace má **typ zdroje**, který určuje, jak se při instalaci získávají soubory aplikace:
| Typ zdroje | Jak se získávají soubory | Typický případ použití |
| ---------- | -------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
| `LOCAL` | Soubory jsou průběžně synchronizovány nástrojem CLI watcher v reálném čase — instalace se přeskočí | Vývoj s `app:dev` |
| `NPM` | Získáváno z registru npm prostřednictvím pole `sourcePackage` | Publikované aplikace na npm |
| `TARBALL` | Extrahováno z nahraného souboru `.tgz` uloženého na serveru | Soukromé aplikace publikované pomocí `--server` |
### Jak probíhá registrace
* **`app:dev`** — při prvním spuštění vývojového režimu pro pracovní prostor automaticky vytvoří registraci `LOCAL`.
* **`app:publish --server`** — nahraje tarball a vytvoří (nebo aktualizuje) registraci `TARBALL` a poté nainstaluje aplikaci.
* **tržiště npm** — registrace `NPM` se vytvářejí, když jsou aplikace synchronizovány z registru npm do katalogu tržiště Twenty.
* **GraphQL API** — registrace můžete vytvářet také programově pomocí mutace `createApplicationRegistration`.
### Registrace vs instalace
**Registrace** a **instalace** jsou odlišné pojmy:
* **Registrace** (`ApplicationRegistration`) je globální záznam metadat popisující aplikaci: její název, typ zdroje, přihlašovací údaje OAuth a stav zařazení na tržišti. Existuje nezávisle na jakémkoli pracovním prostoru.
* **Instalace** (`Application`) je instancí na úrovni pracovního prostoru. Když uživatel nainstaluje aplikaci, Twenty načte balíček ze zdroje uvedeného v registraci, zapíše sestavené soubory do úložiště a synchronizuje manifest (vytváření objektů, polí, logických funkcí atd.) v daném pracovním prostoru.
Jedna registrace může být nainstalována v mnoha pracovních prostorech. Každý pracovní prostor získá svou vlastní kopii souborů a datového modelu aplikace.
### Přihlašovací údaje OAuth
Každá registrace obsahuje přihlašovací údaje OAuth (`oAuthClientId` a `oAuthClientSecret`) vygenerované při vytvoření. Tyto údaje aplikace používá k autentizaci požadavků na API jménem uživatelů. Tajný klíč klienta je při vytvoření vrácen **pouze jednou** — uložte jej bezpečně. Později jej můžete rotovat prostřednictvím mutace `rotateApplicationRegistrationClientSecret`.
## Ruční nastavení (bez scaffolderu)
Ačkoli pro nejlepší začátky doporučujeme použít `create-twenty-app`, projekt můžete nastavit i ručně. Neinstalujte CLI globálně. Místo toho přidejte `twenty-sdk` jako lokální závislost a přidejte jeden skript do souboru package.json:
@@ -943,13 +1415,13 @@ Poté přidejte skript `twenty`:
}
```
Nyní můžete spouštět všechny příkazy přes `yarn twenty <command>`, např. `yarn twenty app:dev`, `yarn twenty help` atd.
Now you can run all commands via `yarn twenty <command>`, e.g. `yarn twenty dev`, `yarn twenty help`, etc.
## Řešení potíží
* Chyby ověření: spusťte `yarn twenty auth:login` a ujistěte se, že váš klíč API má požadovaná oprávnění.
* Nelze se připojit k serveru: ověřte URL API a že je server Twenty dosažitelný.
* Types or client missing/outdated: restart `yarn twenty app:dev` — it auto-generates the typed client.
* Režim vývoje se nesynchronizuje: ujistěte se, že běží `yarn twenty app:dev` a že vaše prostředí změny neignoruje.
* Types or client missing/outdated: restart `yarn twenty dev` — it auto-generates the typed client.
* Dev mode not syncing: ensure `yarn twenty dev` is running and that changes are not ignored by your environment.
Kanál podpory na Discordu: https://discord.com/channels/1130383047699738754/1130386664812982322
@@ -1,7 +1,6 @@
---
title: Rozšiřte
description: Rozšiřte funkčnost Twenty pomocí rozhraní API, webhooků a vlastních aplikací.
redirect: /developers/introduction
---
<Frame>
@@ -16,18 +15,18 @@ Twenty je navrženo tak, aby bylo rozšiřitelné. Použijte naše rozhraní API
* **API**: Programově dotazujte a upravujte svá CRM data pomocí REST nebo GraphQL
* **Webhooky**: Dostávejte oznámení v reálném čase, když v Twenty dojde k událostem
* **Aplikace**: Vytvářejte vlastní aplikace, které rozšiřují možnosti Twenty - Již brzy!
* **Aplikace**: Vytvářejte vlastní aplikace, které rozšiřují možnosti Twenty
## Začínáme
<CardGroup cols={2}>
<Card title="API" icon="kód" href="/l/cs/developers/api">
<Card title="API" icon="kód" href="/l/cs/developers/extend/api">
Programově se připojte k Twenty
</Card>
<Card title="Webhooky" icon="bell" href="/l/cs/developers/webhooks">
<Card title="Webhooky" icon="bell" href="/l/cs/developers/extend/webhooks">
Dostávejte oznámení o událostech v reálném čase
</Card>
<Card title="Aplikace" icon="puzzle-piece" href="/l/cs/developers/apps/apps">
Vytvářejte přizpůsobení jako kód (Alpha)
<Card title="Aplikace" icon="puzzle-piece" href="/l/cs/developers/extend/apps/getting-started">
Vytvářejte přizpůsobení jako kód
</Card>
</CardGroup>
@@ -0,0 +1,116 @@
---
title: Webhooky
description: Dostávejte oznámení v reálném čase, když ve vašem CRM dojde k událostem.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
Webhooky posílají data do vašich systémů v reálném čase, když v Twenty dojde k událostem — bez potřeby průběžného dotazování. Použijte je k udržování externích systémů v synchronizaci, spouštění automatizací nebo zasílání upozornění.
## Vytvořit Webhook
1. Přejděte na **Nastavení → API & Webhooks → Webhooks**
2. Klikněte na **+ Vytvořit webhook**
3. Zadejte adresu URL svého webhooku (musí být veřejně přístupná)
4. Klikněte na **Uložit**
Webhook se aktivuje okamžitě a začne odesílat oznámení.
<VimeoEmbed videoId="928786708" title="Vytvoření webhooku" />
### Spravovat Webhooky
**Upravit**: Klikněte na webhook → Aktualizovat adresu URL → **Uložit**
**Odstranit**: Klikněte na webhook → **Odstranit** → Potvrdit
## Události
Twenty odesílá webhooky pro tyto typy událostí:
| Událost | Příklad |
| -------------------------- | ---------------------------------------------------------- |
| **Záznam je vytvořen** | `person.created`, `company.created`, `note.created` |
| **Záznam je aktualizován** | `person.updated`, `company.updated`, `opportunity.updated` |
| **Záznam je odstraněn** | `person.deleted`, `company.deleted` |
Všechny typy událostí jsou odesílány na adresu URL vašeho webhooku. Filtrování událostí může být přidáno v budoucích verzích.
## Formát payloadu
Každý webhook odesílá HTTP POST s tělem ve formátu JSON:
```json
{
"event": "person.created",
"data": {
"id": "abc12345",
"firstName": "Alice",
"lastName": "Doe",
"email": "alice@example.com",
"createdAt": "2025-02-10T15:30:45Z",
"createdBy": "user_123"
},
"timestamp": "2025-02-10T15:30:50Z"
}
```
| Pole | Popis |
| ----------- | ------------------------------------------------------ |
| `event` | Co se stalo (např. `person.created`) |
| `data` | Celý záznam, který byl vytvořen/aktualizován/odstraněn |
| `timestamp` | Kdy k události došlo (UTC) |
<Note>
Odpovězte se **stavem HTTP 2xx** (200299) pro potvrzení přijetí. Odpovědi mimo 2xx se zaznamenávají jako selhání doručení.
</Note>
## Ověření Webhooku
Twenty kvůli bezpečnosti podepisuje každý požadavek webhooku. Ověřujte podpisy, abyste zajistili, že požadavky jsou autentické.
### Záhlaví
| Hlavička | Popis |
| ---------------------------- | ------------------------ |
| `X-Twenty-Webhook-Signature` | Podpis HMAC SHA256 |
| `X-Twenty-Webhook-Timestamp` | Časové razítko požadavku |
### Kroky ověření
1. Získejte časové razítko z `X-Twenty-Webhook-Timestamp`
2. Vytvořte řetězec: `{timestamp}:{JSON payload}`
3. Vypočítejte HMAC SHA256 pomocí vašeho tajného klíče webhooku
4. Porovnejte s `X-Twenty-Webhook-Signature`
### Příklad (Node.js)
```javascript
const crypto = require("crypto");
const timestamp = req.headers["x-twenty-webhook-timestamp"];
const payload = JSON.stringify(req.body);
const secret = "your-webhook-secret";
const stringToSign = `${timestamp}:${payload}`;
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(stringToSign)
.digest("hex");
const receivedSignature = req.headers["x-twenty-webhook-signature"];
const isValid = crypto.timingSafeEqual(
Buffer.from(expectedSignature, "hex"),
Buffer.from(receivedSignature, "hex")
);
```
## Webhooky vs. pracovní postupy
| Metoda | Směr | Případ použití |
| ----------------------------------------- | ---- | --------------------------------------------------------------- |
| **Webhooky** | OUT | Automaticky informujte externí systémy o jakékoli změně záznamu |
| **Pracovní postup + požadavek HTTP** | OUT | Odesílejte data ven s vlastní logikou (filtry, transformace) |
| **Spouštěč webhooku v pracovním postupu** | IN | Přijímejte data do Twenty z externích systémů |
Pro příjem externích dat viz [Nastavení spouštěče webhooku](/l/cs/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger).
@@ -5,28 +5,18 @@ description: Vítejte v dokumentaci pro vývojáře Twenty, která je vaším zd
import { CardTitle } from "/snippets/card-title.mdx"
<CardGroup cols={2}>
<Card href="/l/cs/developers/api" icon="code">
<CardTitle>API</CardTitle>
Dotazujte a upravujte data CRM pomocí REST nebo GraphQL.
<CardGroup cols={3}>
<Card href="/l/cs/developers/extend/extend" img="/images/user-guide/integrations/plug.png">
<CardTitle>Rozšiřte</CardTitle>
Vytvářejte integrace pomocí API, webhooků a vlastních aplikací.
</Card>
<Card href="/l/cs/developers/webhooks" icon="bell">
<CardTitle>Webhooks</CardTitle>
Přijímejte oznámení v reálném čase při výskytu událostí.
</Card>
<Card href="/l/cs/developers/apps/apps" icon="puzzle-piece">
<CardTitle>Apps</CardTitle>
Vytvářejte vlastní aplikace rozšiřující možnosti Twenty.
</Card>
<Card href="/l/cs/developers/self-host/self-host" icon="desktop">
<Card href="/l/cs/developers/self-host/self-host" img="/images/user-guide/what-is-twenty/20.png">
<CardTitle>Hostujte sami</CardTitle>
Nasaďte a spravujte Twenty na vlastní infrastruktuře.
</Card>
<Card href="/l/cs/developers/contribute/contribute" icon="github">
<Card href="/l/cs/developers/contribute/contribute" img="/images/user-guide/github/github-header.png">
<CardTitle>Přispějte</CardTitle>
Připojte se k naší open-source komunitě a přispívejte do Twenty.
</Card>
@@ -3,7 +3,7 @@ title: Docker Compose jedním kliknutím
---
<Warning>
Docker kontejnery jsou určeny pro produkční hosting nebo samohostování; pro přispívání si prosím přečtěte [Lokální nastavení](/l/cs/developers/contribute/capabilities/local-setup).
Docker kontejnery jsou určeny pro produkční provoz nebo vlastní hostování. Pro přispívání se podívejte na [Lokální nastavení](/l/cs/developers/contribute/capabilities/local-setup).
</Warning>
## Přehled
@@ -12,7 +12,7 @@ Tato příručka poskytuje podrobný návod k instalaci a konfiguraci aplikace T
**Důležité:** Měňte pouze nastavení výslovně uvedená v tomto průvodci. Změna jiných konfigurací může vést k problémům.
Podívejte se na dokumentaci [Nastavení Proměnných Prostředí](/l/cs/developers/self-host/capabilities/setup) pro pokročilou konfiguraci. Všechny proměnné prostředí musí být deklarovány v souboru docker-compose.yml na úrovni serveru a/nebo pracovníka v závislosti na proměnné.
Viz [Nastavení proměnných prostředí](/l/cs/developers/self-host/capabilities/setup) pro pokročilou konfiguraci. Všechny proměnné prostředí musí být deklarovány v souboru `docker-compose.yml` na úrovni serveru a/nebo pracovníka, v závislosti na proměnné.
## Systémové Požadavky
@@ -297,46 +297,60 @@ yarn command:prod cron:workflow:automated-cron-trigger
**Režim pouze s prostředím:** Pokud nastavíte `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`, přidejte tyto proměnné do souboru `.env`
</Warning>
## Logické funkce
## Logické funkce a interpret kódu
Twenty podporuje logické funkce pro pracovní postupy a vlastní logiku. Běhové prostředí se konfiguruje pomocí proměnné prostředí `SERVERLESS_TYPE`.
Twenty podporuje logické funkce pro pracovní postupy a interpret kódu pro analýzu dat s využitím AI. Obě spouštějí kód poskytnutý uživatelem a z důvodu zabezpečení vyžadují explicitní konfiguraci.
### Výchozí nastavení zabezpečení
**V produkci (NODE_ENV=production):** Jak logické funkce, tak interpret kódu mají ve výchozím nastavení stav **Zakázáno**. Pokud tyto funkce potřebujete, musíte je explicitně povolit pomocí `LOGIC_FUNCTION_TYPE` a `CODE_INTERPRETER_TYPE`.
**V prostředí vývoje (NODE_ENV=development):** Obě mají ve výchozím nastavení **LOCAL** pro pohodlí při lokálním spuštění.
<Warning>
**Upozornění na zabezpečení:** Místní ovladač (`SERVERLESS_TYPE=LOCAL`) spouští kód přímo na hostiteli v procesu Node.js bez sandboxu. Měl by být používán pouze pro důvěryhodný kód při vývoji. Pro produkční nasazení, která pracují s nedůvěryhodným kódem, důrazně doporučujeme použít `SERVERLESS_TYPE=LAMBDA` nebo `SERVERLESS_TYPE=DISABLED`.
**Upozornění na zabezpečení:** Místní ovladač (`LOGIC_FUNCTION_TYPE=LOCAL` nebo `CODE_INTERPRETER_TYPE=LOCAL`) spouští kód přímo na hostiteli v procesu Node.js bez sandboxu. Měl by být používán pouze pro důvěryhodný kód při vývoji. Pro produkční nasazení, která pracují s nedůvěryhodným kódem, použijte `LOGIC_FUNCTION_TYPE=LAMBDA` nebo `CODE_INTERPRETER_TYPE=E2B` (se sandboxem), případně je ponechte vypnuté.
</Warning>
### Dostupné ovladače
### Logické funkce - dostupné ovladače
| Ovladač | Proměnná prostředí | Případ použití | Úroveň zabezpečení |
| --------- | -------------------------- | ------------------------------------------ | ----------------------------------- |
| Neaktivní | `SERVERLESS_TYPE=DISABLED` | Úplně zakázat logické funkce | N/A |
| Lokální | `SERVERLESS_TYPE=LOCAL` | Vývojová a důvěryhodná prostředí | Nízká (bez sandboxu) |
| Lambda | `SERVERLESS_TYPE=LAMBDA` | Produkční prostředí s nedůvěryhodným kódem | Vysoká (izolace na úrovni hardwaru) |
| Ovladač | Proměnná prostředí | Případ použití | Úroveň zabezpečení |
| --------- | ------------------------------ | ------------------------------------------ | ----------------------------------- |
| Neaktivní | `LOGIC_FUNCTION_TYPE=DISABLED` | Úplně zakázat logické funkce | N/A |
| Lokální | `LOGIC_FUNCTION_TYPE=LOCAL` | Vývojová a důvěryhodná prostředí | Nízká (bez sandboxu) |
| Lambda | `LOGIC_FUNCTION_TYPE=LAMBDA` | Produkční prostředí s nedůvěryhodným kódem | Vysoká (izolace na úrovni hardwaru) |
### Doporučená konfigurace
### Logické funkce - doporučená konfigurace
**Pro vývoj:**
```bash
SERVERLESS_TYPE=LOCAL # default
LOGIC_FUNCTION_TYPE=LOCAL # default when NODE_ENV=development
```
**Pro produkci (AWS):**
```bash
SERVERLESS_TYPE=LAMBDA
SERVERLESS_LAMBDA_REGION=us-east-1
SERVERLESS_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
SERVERLESS_LAMBDA_ACCESS_KEY_ID=your-access-key
SERVERLESS_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
LOGIC_FUNCTION_TYPE=LAMBDA
LOGIC_FUNCTION_LAMBDA_REGION=us-east-1
LOGIC_FUNCTION_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
LOGIC_FUNCTION_LAMBDA_ACCESS_KEY_ID=your-access-key
LOGIC_FUNCTION_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
```
**Pro zakázání logických funkcí:**
```bash
SERVERLESS_TYPE=DISABLED
LOGIC_FUNCTION_TYPE=DISABLED # default when NODE_ENV=production
```
### Interpret kódu - dostupné ovladače
| Ovladač | Proměnná prostředí | Případ použití | Úroveň zabezpečení |
| --------- | -------------------------------- | ----------------------------------------------- | -------------------------- |
| Neaktivní | `CODE_INTERPRETER_TYPE=DISABLED` | Zakázat spouštění kódu AI | N/A |
| Lokální | `CODE_INTERPRETER_TYPE=LOCAL` | Pouze pro vývoj | Nízká (bez sandboxu) |
| E2B | `CODE_INTERPRETER_TYPE=E_2_B` | Produkční prostředí se sandboxovaným spouštěním | Vysoká (izolovaný sandbox) |
<Note>
Při použití `SERVERLESS_TYPE=DISABLED` skončí každý pokus o spuštění logické funkce chybou. To je užitečné, pokud chcete provozovat Twenty bez podpory logických funkcí.
Při použití `LOGIC_FUNCTION_TYPE=DISABLED` nebo `CODE_INTERPRETER_TYPE=DISABLED` vrátí každý pokus o spuštění chybu. To je užitečné, pokud chcete provozovat Twenty bez těchto schopností.
</Note>
@@ -167,6 +167,10 @@ plugins: [
Spusťte `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = 'you@yourdomain.com';` v databázovém kontejneru pro získání přístupu k administračnímu panelu.
#### Při spuštění workflow dojde k chybě "Logic function execution is disabled. Set LOGIC_FUNCTION_TYPE to LOCAL or LAMBDA to enable."
V produkčním prostředí jsou logické funkce ve výchozím nastavení zakázané. Nastavte proměnnou prostředí `LOGIC_FUNCTION_TYPE` na `LOCAL` nebo `LAMBDA`, abyste je povolili. Toto lze nakonfigurovat pomocí proměnných prostředí nebo prostřednictvím proměnných databáze v administračním panelu. Podrobnosti najdete v [průvodci nastavením Logic Functions](/l/cs/developers/self-host/capabilities/setup#logic-functions-available-drivers).
### Docker Compose jedním kliknutím
#### Nelze se přihlásit
+2 -2
View File
@@ -149,8 +149,8 @@
"extend": {
"label": "Rozšíření",
"groups": {
"extendCapabilities": {
"label": "Možnosti"
"apps": {
"label": "Aplikace"
}
}
},
@@ -0,0 +1,142 @@
---
title: MCP Server
description: Připojte asistenty AI ke svému pracovnímu prostoru Twenty pomocí protokolu Model Context Protocol.
---
<Warning>
MCP je v současnosti ve fázi **alpha** a je dostupný pouze v některých pracovních prostorech. MCP nemusí být ve vašem pracovním prostoru ještě povolen.
</Warning>
Twenty zpřístupňuje server [MCP](https://modelcontextprotocol.io/), aby AI asistenti — Claude Desktop, Claude Code, Cursor, ChatGPT a další — mohli číst a zapisovat data vašeho CRM pomocí přirozeného jazyka.
Použijte svou **adresu URL pracovní oblasti** (adresu URL, kterou používáte k přístupu k Twenty) jako koncový bod MCP. V Twenty Cloud může vaše adresa URL pracovní oblasti být `https://{mycompany}.twenty.com` nebo vlastní doména. Server je dostupný na:
| Prostředí | Koncový bod MCP |
| ------------------- | ----------------------------------------------------------------------------- |
| **Cloud** | `https://{your-workspace-url}/mcp` (např. `https://mycompany.twenty.com/mcp`) |
| **Vlastní hosting** | `https://{your-domain}/mcp` |
## Metody ověření
Máte dva způsoby, jak ověřit svého klienta MCP: **OAuth** (doporučeno) nebo **klíč API**.
### Možnost A — OAuth (doporučeno)
S OAuth váš klient MCP otevře okno prohlížeče, abyste se mohli přihlásit. V konfiguračních souborech se neukládají žádné tajné údaje a tokeny se automaticky obnovují.
<Note>
OAuth vyžaduje klienta MCP, který podporuje [specifikaci autorizace MCP](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization). Podporují ji Claude Desktop, Claude Code, Cursor a ChatGPT.
</Note>
Přidejte toto do konfigurace svého klienta MCP a nahraďte `{your-workspace-url}` hostitelem vaší pracovní oblasti (např. `mycompany.twenty.com`):
```json
{
"mcpServers": {
"twenty": {
"type": "streamable-http",
"url": "https://{your-workspace-url}/mcp"
}
}
}
```
To je vše — není potřeba žádný klíč API. Když se klient připojí poprvé, provede:
1. Získá metadata OAuth služby Twenty pomocí `/.well-known/oauth-protected-resource` a `/.well-known/oauth-authorization-server`
2. Zaregistruje se jako klient OAuth prostřednictvím dynamické registrace klienta (RFC 7591)
3. Otevře váš prohlížeč k autorizaci přístupu
4. Převezme tokeny a připojí se k serveru MCP
Následující připojení znovu použijí uložené tokeny a automaticky je obnoví.
### Možnost B — klíč API
Pokud váš klient MCP nepodporuje OAuth, nebo dáváte přednost statickým přihlašovacím údajům, předejte klíč API v hlavičce `Authorization`:
```json
{
"mcpServers": {
"twenty": {
"type": "streamable-http",
"url": "https://{your-workspace-url}/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}
```
<Warning>
Váš klíč API poskytuje přístup k datům pracovní oblasti. Nenechávejte jej ve verzovacím systému ani ve sdílených dotfiles.
</Warning>
Chcete-li vytvořit klíč API, přejděte do **Settings > APIs & Webhooks > + Create key**. Podrobnosti viz [Rozhraní API](/l/cs/developers/extend/api#create-an-api-key).
## Rychlý start
### 1. Zkopírujte konfiguraci
V Twenty přejděte do **Settings > AI > More > MCP Server**. Vyberte metodu ověření (OAuth nebo klíč API), zkopírujte úryvek JSON (už bude používat adresu URL vaší pracovní oblasti) a vložte jej do konfiguračního souboru klienta MCP.
| Klient | Umístění konfiguračního souboru |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Claude Desktop** | `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) nebo `%APPDATA%\Claude\claude_desktop_config.json` (Windows) |
| **Claude Code** | `~/.claude.json` (uživatel) nebo `.mcp.json` (projekt) |
| **Cursor** | `.cursor/mcp.json` ve vašem projektu nebo `~/.cursor/mcp.json` globálně |
| **ChatGPT** | Zapněte Developer Mode v **Settings > Apps & Connectors > Advanced settings**, poté použijte **Create** v **Settings > Apps & Connectors** k přidání serveru MCP |
### 2. Připojení
Restartujte svého klienta MCP (nebo znovu načtěte konfiguraci). Pokud používáte OAuth, budete přesměrováni do Twenty k autorizaci přístupu. Pokud používáte klíč API, připojení je okamžité.
### 3. Začněte jej používat
Požádejte svého AI asistenta, aby pracoval s vaším CRM:
* *"Ukaž mi 5 nejnověji vytvořených společností"*
* *"Vytvoř novou osobu jménem Jane Doe ve společnosti Acme Corp"*
* *"Najdi všechny otevřené příležitosti s hodnotou vyšší než 10 000 $"*
## Dostupné nástroje
Po připojení server MCP zpřístupní nástroje, které odpovídají rozhraní Twenty API. Doporučený pracovní postup je:
1. **`get_tool_catalog`** — zjistit všechny dostupné nástroje
2. **`learn_tools`** — získat vstupní schéma pro konkrétní nástroje
3. **`execute_tool`** — spustit nástroj
Není potřeba si pamatovat názvy nástrojů. Zeptejte se svého AI asistenta, co umí, a automaticky zavolá `get_tool_catalog`.
## Oprávnění
Připojení MCP dědí oprávnění ověřeného uživatele (OAuth) nebo roli přiřazenou ke klíči API. Chcete-li omezit, co může server MCP provádět:
* **OAuth**: Platí role uživatele v pracovní oblasti.
* **Klíč API**: Přiřaďte klíči API roli v **Settings > Roles**. Viz [Oprávnění](/l/cs/user-guide/permissions-access/capabilities/permissions).
## Konfigurace pro self-hosting
U samostatně hostovaných instancí nahraďte `{your-workspace-url}` adresou URL svého serveru. Ujistěte se, že `SERVER_URL` ve vašem prostředí odpovídá veřejné adrese URL vaší instance Twenty — tato hodnota se používá k vygenerování metadat pro zjišťování OAuth.
```bash
SERVER_URL=https://twenty.yourcompany.com
```
Koncový bod MCP, koncové body OAuth a metadata pro zjišťování se z této hodnoty odvozují.
## Řešení potíží
**Chyby "Unauthorized" nebo 401**
* OAuth: Proveďte znovu autorizaci vymazáním uložených tokenů ve vašem klientovi MCP a znovu se připojte.
* Klíč API: Ověřte, že je klíč platný a nevypršela mu platnost. V případě potřeby jej znovu vygenerujte.
**Tok OAuth neotevírá prohlížeč**
* Ujistěte se, že váš klient MCP podporuje autorizaci MCP. Pokud ne, použijte metodu s klíčem API.
**Časový limit připojení**
* Ověřte, že je adresa URL koncového bodu MCP dosažitelná z vašeho počítače. U samostatně hostovaných instancí zkontrolujte, že server běží a že je `SERVER_URL` nastaven správně.
@@ -24,7 +24,7 @@ Exportujte data svého pracovního prostoru do CSV pro zálohování, vytvářen
* Exportují se pouze **viditelné sloupce**
* Exportují se pouze **filtrované záznamy** (podle vašeho aktuálního zobrazení)
<Note>U větších exportů (20 000+ záznamů) použijte filtry pro export po dávkách nebo použijte [API](/l/cs/developers/api).</Note>
<Note>U větších exportů (20 000+ záznamů) použijte filtry pro export po dávkách nebo použijte [API](/l/cs/developers/extend/api).</Note>
### Oprávnění
@@ -148,7 +148,7 @@ API nemá limit počtu záznamů:
2. Použijte GraphQL API k dotazování na záznamy
3. Zpracujte výsledky ve své aplikaci
Viz: [Dokumentace API](/l/cs/developers/api)
Viz: [Dokumentace API](/l/cs/developers/extend/api)
## Tipy a osvědčené postupy
@@ -206,4 +206,4 @@ Exportované soubory mohou obsahovat citlivá data:
* [Jak aktualizovat existující záznamy](/l/cs/user-guide/data-migration/how-tos/update-existing-records-via-import) — upravte a znovu importujte svůj export
* [Jak importovat data přes API](/l/cs/user-guide/data-migration/how-tos/import-data-via-api) — pro velké datové sady
* [Dokumentace API](/l/cs/developers/api) — vytvářejte vlastní pracovní postupy pro export
* [Dokumentace API](/l/cs/developers/extend/api) — vytvářejte vlastní pracovní postupy pro export
@@ -57,10 +57,10 @@ Kdokoli s vaším klíčem API může přistupovat k datům vašeho pracovního
Twenty podporuje dva typy API:
| API | Vhodné pro | Dokumentace |
| ----------- | ----------------------------------------------------------------- | ------------------------------------------------------- |
| **GraphQL** | Flexibilní dotazy, získávání souvisejících dat, komplexní operace | [Dokumentace API](/l/cs/developers/api) |
| **REST** | Jednoduché CRUD operace, známé postupy REST | [Dokumentace API](/l/cs/developers/api) |
| API | Vhodné pro | Dokumentace |
| ----------- | ----------------------------------------------------------------- | ----------------------------------------- |
| **GraphQL** | Flexibilní dotazy, získávání souvisejících dat, komplexní operace | [Dokumentace API](/l/cs/developers/extend/api) |
| **REST** | Jednoduché CRUD operace, známé postupy REST | [Dokumentace API](/l/cs/developers/extend/api) |
Obě API podporují:
@@ -173,4 +173,4 @@ Kontaktujte nás na [contact@twenty.com](mailto:contact@twenty.com) nebo prozkou
Úplné podrobnosti implementace, ukázky kódu a referenci schématu najdete zde:
* [Dokumentace API](/l/cs/developers/api)
* [Dokumentace API](/l/cs/developers/extend/api)
@@ -35,7 +35,7 @@ Open-source je základ našeho přístupu, zajišťuje, že Twenty se vyvíjí s
* **Přehledy:** Sledujte výkon pomocí vlastních sestav a vizualizací. [Zobrazit přehledy](/l/cs/user-guide/dashboards/overview).
* **Oprávnění a přístup:** Ovládejte, kdo může zobrazit, upravovat a spravovat vaše data pomocí oprávnění založených na rolích. [Nastavte přístup](/l/cs/user-guide/permissions-access/overview).
* **Poznámky a úkoly:** Vytvářejte poznámky a úkoly propojené s vašimi záznamy pro lepší spolupráci.
* **API & Webhooks:** Připojte se k dalším aplikacím a vytvářejte vlastní integrace. [Začněte integraci](/l/cs/developers/api).
* **API & Webhooks:** Připojte se k dalším aplikacím a vytvářejte vlastní integrace. [Začněte integraci](/l/cs/developers/extend/api).
## Připojte se nyní
@@ -0,0 +1,121 @@
---
title: Automatická odpověď na příchozí e-maily
description: Vytvořte pracovní postup, který pomocí AI třídí příchozí e-maily a automaticky odesílá odpovědi ve vláknu.
---
Odpovídejte na příchozí e-maily během několika sekund — ne hodin. Tento pracovní postup využívá AI agenta k odfiltrování šumu (newslettery, spam, automatické odpovědi) a k vytvoření personalizované odpovědi na skutečné zprávy, kterou poté odešle jako odpověď ve vláknu v rámci původní konverzace.
## Jak funguje vláknování e-mailů
Každý e-mail obsahuje skrytou hlavičku `Message-ID` — jedinečný identifikátor přiřazený poštovním serverem odesílatele. Když odpovíte na e-mail, váš poštovní klient nastaví hlavičku `In-Reply-To`, která odkazuje na tento otisk. Takto Gmail, Outlook a další klienti seskupují zprávy do vláken.
V Twenty je tento otisk uložen jako `headerMessageId` na objektu Message. Váš pracovní postup jej vezme a předá do pole In-Reply-To akce Odeslat e-mail.
## Sestavení pracovního postupu
### Krok 1: Vytvořte nový pracovní postup
Přejděte do **Nastavení -> Pracovní postupy** a klikněte na **+ Nový pracovní postup**.
### Krok 2: Spouštění při příchozích zprávách
Vyberte **Když je vytvořen záznam** a zvolte **Zprávy**.
Pokaždé, když do Twenty dorazí e-mail, toto se spustí.
<img src="/images/user-guide/workflows/auto-reply/workflow-overview.png" style={{width:'100%'}} />
### Krok 3: Vyhledejte, kdo to poslal
Přidejte akci **Vyhledat záznamy**.
Adresa odesílatele není přímo v samotné zprávě — je v souvisejícím záznamu Message Participant.
| Pole | Hodnota |
| ---------- | ------------------------------ |
| **Objekt** | Účastníci zprávy |
| **Filtr** | Zpráva **je** `{{trigger.id}}` |
| **Filtr** | Role **je** From |
| **Limit** | 1 |
To vám poskytne e-mail odesílatele v `handle` a jeho jméno v `displayName`.
<img src="/images/user-guide/workflows/auto-reply/find-sender.png" style={{width:'100%'}} />
### Krok 4: Třídění pomocí AI a návrh odpovědi
Přidejte akci **AI Agent**. Tento jediný krok dělá dvě věci: rozhodne, zda si e-mail zaslouží odpověď, a pokud ano, napíše ji.
Použijte prompt například takto:
```
You are an email triage assistant for a sales team. Read the following
inbound email and decide if it deserves a reply.
Subject: {{trigger.subject}}
Body: {{trigger.text}}
From: {{Find Sender.first.displayName}} ({{Find Sender.first.handle}})
If this email is spam, a newsletter, an automated notification, or
otherwise does not need a human reply, respond with exactly: SKIP
Otherwise, write a short, professional reply (3-4 sentences max) that:
- Acknowledges their specific message
- Lets them know someone from the team will follow up shortly
- Is warm but not overly casual
Respond with only the reply text, no subject line or greeting prefix.
```
AI Agent vypíše svou odpověď do pole `response`, na které mohou navazující kroky odkazovat.
<img src="/images/user-guide/workflows/auto-reply/ai-triage.png" style={{width:'100%'}} />
### Krok 5: Rozvětvěte podle rozhodnutí AI
Přidejte akci **If/Else** k ověření, zda se AI rozhodla odpovědět, nebo přeskočit.
| Pole | Hodnota |
| ------------ | ----------------------------------------- |
| **Podmínka** | AI Agent `response` **neobsahuje** `SKIP` |
| **If true** | Pokračovat na Odeslat e-mail |
| **Else** | Nedělat nic (pracovní postup končí) |
Spam, newslettery a automaticky generované zprávy se zahodí. Vše ostatní přejde do dalšího kroku.
<img src="/images/user-guide/workflows/auto-reply/should-reply.png" style={{width:'100%'}} />
### Krok 6: Odeslat odpověď ve vlákně
Přidejte akci **Odeslat e-mail** ve větvi "if true". Klikněte na **Pokročilé možnosti** a poté na **Přidat In-Reply-To**.
| Pole | Hodnota |
| --------------- | -------------------------------------- |
| **Komu** | `{{Find Sender.first.handle}}` |
| **Předmět** | `Re: {{trigger.subject}}` |
| **Text zprávy** | `{{AI Triage & Draft Reply.response}}` |
| **In-Reply-To** | `{{trigger.headerMessageId}}` |
Pole In-Reply-To je to, co z toho dělá odpověď místo nové konverzace. Příjemce ji uvidí zařazenou pod původní e-mail ve službě Gmail, v Outlooku nebo v jiném klientovi.
<img src="/images/user-guide/workflows/auto-reply/send-email.png" style={{width:'100%'}} />
<Tip>
**In-Reply-To** očekává ze spouštěče `message.headerMessageId` — jde o jedinečný otisk e-mailu, nikoli adresu příjemce. Pokud jej necháte prázdné, e-mail se i tak odešle, jen jako samostatná zpráva.
</Tip>
<Warning>
Gmail používá předmět k seskupování zpráv do vláken. Předmět **musí** začínat `Re:` (včetně dvojtečky a mezery), aby Gmail zobrazil odpověď v rámci původního vlákna. Bez toho se odpověď zobrazí jako samostatná konverzace — i když je hlavička In-Reply-To nastavena správně.
</Warning>
### Krok 7: Otestujte a aktivujte
Klikněte na **Test**, poté zkontrolujte svého e-mailového klienta. Odpověď by se měla zobrazit vnořená pod původní zprávou.
Aktivujte, až s tím budete spokojeni.
## Nápady k rozšíření
* **Odpovídat pouze VIP** — přidejte větev, která zkontroluje doménu odesílatele nebo zda existuje jako Kontakt v Twenty
* **Směrovat podle záměru** — použijte samostatné prompty pro AI Agenta k odlišnému zpracování obchodních dotazů oproti požadavkům na podporu
* **Obohaťte před odpovědí** — přidejte krok Vyhledat záznamy, který do promptu AI načte společnost odesílatele nebo historii obchodů pro osobnější odpovědi
@@ -95,7 +95,7 @@ Vytvořte cílová pole v **Nastavení → Datový model → Příležitosti**:
* Velikost společnosti: `{{searchRecords[0].employees}}`
<Note>
**Omezení pro Úkoly a Poznámky**: Relace u Úkolů a Poznámek jsou napevno nastaveny jako mnoho k mnoha a zatím nejsou k dispozici ve spouštěčích ani akcích pracovních postupů. Pro přístup k těmto relacím použijte místo toho [API](/l/cs/developers/api).
**Omezení pro Úkoly a Poznámky**: Relace u Úkolů a Poznámek jsou napevno nastaveny jako mnoho k mnoha a zatím nejsou k dispozici ve spouštěčích ani akcích pracovních postupů. Pro přístup k těmto relacím použijte místo toho [API](/l/cs/developers/extend/api).
</Note>
## Oboustranná synchronizace
@@ -563,7 +563,7 @@ Hauptpunkte:
* Frontend-Komponenten sind React-Komponenten, die in isolierten Kontexten innerhalb von Twenty gerendert werden.
* Das Feld `component` verweist auf Ihre React-Komponente.
* Komponenten werden während `yarn twenty app:dev` automatisch gebaut und synchronisiert.
* Komponenten werden während `yarn twenty dev` automatisch gebaut und synchronisiert.
Sie können neue Frontend-Komponenten auf zwei Arten erstellen:
@@ -607,7 +607,7 @@ Sie können neue Skills auf zwei Arten erstellen:
### Generierte typisierte Clients
Zwei typisierte Clients werden von `yarn twenty app:dev` automatisch generiert und basierend auf Ihrem Arbeitsbereichs-Schema in `node_modules/twenty-sdk/generated` gespeichert:
Zwei typisierte Clients werden von `yarn twenty dev` automatisch generiert und basierend auf Ihrem Arbeitsbereichs-Schema in `node_modules/twenty-sdk/generated` gespeichert:
* **`CoreApiClient`** — fragt den `/graphql`-Endpunkt nach Arbeitsbereichsdaten ab
* **`MetadataApiClient`** — ruft über den Endpunkt `/metadata` die Arbeitsbereichskonfiguration und Datei-Uploads ab.
@@ -622,7 +622,7 @@ const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
```
Beide Clients werden von `yarn twenty app:dev` automatisch neu generiert, sobald sich Ihre Objekte oder Felder ändern.
Beide Clients werden von `yarn twenty dev` automatisch neu generiert, sobald sich Ihre Objekte oder Felder ändern.
#### Laufzeit-Anmeldedaten in Logikfunktionen
@@ -20,7 +20,7 @@ Apps ermöglichen es Ihnen, Twenty mit benutzerdefinierten Objekten, Feldern, Lo
## Voraussetzungen
* Node.js 24+ und Yarn 4
* Ein Twenty-Workspace und ein API-Schlüssel (unter https://app.twenty.com/settings/api-webhooks erstellen)
* Docker (für den lokalen Twenty-Dev-Server)
## Erste Schritte
@@ -32,13 +32,13 @@ npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Start dev mode: automatically syncs local changes to your workspace
yarn twenty app:dev
yarn twenty dev
```
Das Scaffolding-Tool unterstützt zwei Modi, um zu steuern, welche Beispieldateien enthalten sind:
```bash filename="Terminal"
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill)
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent)
npx create-twenty-app@latest my-app
# Minimal: only core files (application-config.ts and default-role.ts)
@@ -64,7 +64,7 @@ yarn twenty function:execute --preInstall
yarn twenty function:execute --postInstall
# Uninstall the application from the current workspace
yarn twenty app:uninstall
yarn twenty uninstall
# Display commands' help
yarn twenty help
@@ -167,7 +167,7 @@ export default defineObject({
Spätere Befehle fügen weitere Dateien und Ordner hinzu:
* `yarn twenty app:dev` generiert automatisch zwei typisierte API-Clients in `node_modules/twenty-sdk/generated`: `CoreApiClient` (für Arbeitsbereichsdaten über `/graphql`) und `MetadataApiClient` (für Arbeitsbereichskonfiguration und Datei-Uploads über `/metadata`).
* `yarn twenty dev` will auto-generate two typed API clients in `node_modules/twenty-sdk/generated`: `CoreApiClient` (for workspace data via `/graphql`) and `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`).
* `yarn twenty entity:add` fügt unter `src/` Entitätsdefinitionsdateien für Ihre benutzerdefinierten Objekte, Funktionen, Frontend-Komponenten, Rollen, Skills und mehr hinzu.
## Authentifizierung
@@ -221,13 +221,25 @@ Fügen Sie dann ein `twenty`-Skript hinzu:
}
```
Jetzt können Sie alle Befehle über `yarn twenty <command>` ausführen, z. B. `yarn twenty app:dev`, `yarn twenty help` usw.
Now you can run all commands via `yarn twenty <command>`, e.g. `yarn twenty dev`, `yarn twenty help`, etc.
## So verwenden Sie eine lokale Twenty-Instanz
Wenn Sie bereits lokal eine Twenty-Instanz ausführen (z. B. über `npx nx start twenty-server`), können Sie sich damit verbinden, anstatt Docker zu verwenden:
```bash filename="Terminal"
# During scaffolding — skip Docker, connect to your running instance
npx create-twenty-app@latest my-app --port 3000
# Or after scaffolding — add a remote pointing to your instance
yarn twenty remote add --local --port 3000
```
## Fehlerbehebung
* Authentifizierungsfehler: Führen Sie `yarn twenty auth:login` aus und stellen Sie sicher, dass Ihr API-Schlüssel die erforderlichen Berechtigungen hat.
* Verbindung zum Server nicht möglich: Überprüfen Sie die API-URL und dass der Twenty-Server erreichbar ist.
* Typen oder Client fehlen/veraltet: Starten Sie `yarn twenty app:dev` neu — der typisierte Client wird automatisch generiert.
* Dev-Modus synchronisiert nicht: Stellen Sie sicher, dass `yarn twenty app:dev` läuft und dass Änderungen von Ihrer Umgebung nicht ignoriert werden.
* Types or client missing/outdated: restart `yarn twenty dev` — it auto-generates the typed client.
* Dev mode not syncing: ensure `yarn twenty dev` is running and that changes are not ignored by your environment.
Discord-Hilfekanal: https://discord.com/channels/1130383047699738754/1130386664812982322
@@ -28,13 +28,13 @@ Die Veröffentlichung auf npm macht Ihre App im Twenty-Marktplatz auffindbar. Je
1. **App erstellen** — die CLI kompiliert Ihre TypeScript-Quellen und erzeugt das Anwendungsmanifest:
```bash filename="Terminal"
yarn twenty app:build
yarn twenty build
```
2. **Auf npm veröffentlichen** — pushen Sie das gebaute Paket in die npm-Registry:
```bash filename="Terminal"
npx twenty app:publish
npx twenty publish
```
### Automatische Erkennung
@@ -65,14 +65,14 @@ jobs:
node-version: "24"
registry-url: https://registry.npmjs.org
- run: yarn install --immutable
- run: npx twenty app:build
- run: npx twenty build
- run: npm publish --provenance --access public
working-directory: .twenty/output
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
Für andere CI-Systeme (GitLab CI, CircleCI usw.) gelten die gleichen drei Befehle: `yarn install`, `npx twenty app:build` und anschließend `npm publish` aus `.twenty/output`.
For other CI systems (GitLab CI, CircleCI, etc.), the same three commands apply: `yarn install`, `npx twenty build`, then `npm publish` from `.twenty/output`.
<Tip>
**npm-Provenance** ist optional, wird jedoch empfohlen. Das Veröffentlichen mit `--provenance` fügt Ihrem npm-Eintrag ein Vertrauensabzeichen hinzu, sodass Nutzer überprüfen können, dass das Paket aus einem bestimmten Commit in einer öffentlichen CI-Pipeline gebaut wurde. Siehe die [npm-Provenance-Dokumentation](https://docs.npmjs.com/generating-provenance-statements) für Einrichtungshinweise.
@@ -87,7 +87,7 @@ Für Apps, die Sie nicht öffentlich verfügbar machen möchten — proprietäre
Erstellen Sie Ihre App und stellen Sie sie in einem Schritt auf einem bestimmten Server bereit:
```bash filename="Terminal"
npx twenty app:publish --server <server-url>
npx twenty publish --server <server-url>
```
Jeder Arbeitsbereich auf diesem Server kann die App anschließend über die Seite **Applications** in den Einstellungen installieren und aktualisieren.
@@ -97,7 +97,7 @@ Jeder Arbeitsbereich auf diesem Server kann die App anschließend über die Seit
So veröffentlichen Sie ein Update:
1. Erhöhen Sie das Feld `version` in Ihrer `package.json`
2. Pushen Sie einen neuen Tarball mit `npx twenty app:publish --server <server-url>`
2. Push a new tarball with `npx twenty publish --server <server-url>`
3. Arbeitsbereiche auf diesem Server sehen in ihren Einstellungen, dass ein Upgrade verfügbar ist.
<Note>
@@ -110,7 +110,7 @@ Twenty organisiert Apps in drei Kategorien, basierend auf ihrer Vertriebsart:
| Kategorie | Wie es funktioniert | Im Marktplatz sichtbar? |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | ----------------------- |
| **Entwicklung** | Lokale Apps im Entwicklungsmodus, die über `yarn twenty app:dev` ausgeführt werden. Zum Erstellen und Testen verwendet. | Nein |
| **Entwicklung** | Local dev mode apps running via `yarn twenty dev`. Zum Erstellen und Testen verwendet. | Nein |
| **Veröffentlicht** | Auf npm veröffentlichte Apps mit dem Präfix `twenty-app-`. Im Marktplatz gelistet, damit jeder Arbeitsbereich sie installieren kann. | Ja |
| **Intern** | Apps, die per Tarball auf einen bestimmten Server bereitgestellt werden. Nur für Arbeitsbereiche auf diesem Server verfügbar. | Nein |
@@ -21,19 +21,51 @@ Mit Apps können Sie Twenty-Anpassungen **als Code** erstellen und verwalten. An
## Voraussetzungen
* Node.js 24+ und Yarn 4
* Ein Twenty-Workspace und ein API-Schlüssel (unter https://app.twenty.com/settings/api-webhooks erstellen)
* Docker (für den lokalen Twenty-Dev-Server)
## Erste Schritte
Erstellen Sie mit dem offiziellen Scaffolder eine neue App, authentifizieren Sie sich und beginnen Sie mit der Entwicklung:
Erstelle eine neue App mit dem offiziellen Scaffolder. Der Scaffolder kann für dich automatisch eine lokale Twenty-Instanz starten:
```bash filename="Terminal"
# Eine neue App erstellen (enthält standardmäßig alle Beispiele)
# Eine neue App erstellen — die CLI bietet an, einen lokalen Twenty-Server zu starten
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Dev-Modus starten: synchronisiert lokale Änderungen automatisch mit deinem Arbeitsbereich
yarn twenty app:dev
yarn twenty dev
```
### Lokale Serververwaltung
Das SDK enthält Befehle zur Verwaltung eines lokalen Twenty-Dev-Servers (All-in-One-Docker-Image mit PostgreSQL, Redis, Server und Worker):
```bash filename="Terminal"
# Den lokalen Server starten (lädt das Image bei Bedarf herunter)
yarn twenty server start
# Serverstatus prüfen
yarn twenty server status
# Serverprotokolle streamen
yarn twenty server logs
# Server stoppen
yarn twenty server stop
# Alle Daten zurücksetzen und neu starten
yarn twenty server reset
```
Der lokale Server ist bereits mit einem Arbeitsbereich und einem Benutzer (`tim@apple.dev` / `tim@apple.dev`) vorbefüllt, sodass Sie ohne manuelle Einrichtung sofort mit der Entwicklung beginnen können.
### Authentifizierung
Verbinden Sie Ihre App mithilfe von OAuth mit dem lokalen Server:
```bash filename="Terminal"
# Authenticate via OAuth (opens browser)
yarn twenty remote add --local
```
Das Scaffolding-Tool unterstützt zwei Modi, um zu steuern, welche Beispieldateien enthalten sind:
@@ -46,34 +78,46 @@ npx create-twenty-app@latest my-app
npx create-twenty-app@latest my-app --minimal
```
### So verwenden Sie eine lokale Twenty-Instanz
Wenn Sie bereits eine lokale Twenty-Instanz ausführen, können Sie sich damit verbinden, anstatt Docker zu verwenden. Geben Sie den Port an, auf dem Ihr lokaler Server lauscht (Standard: `3000`):
```bash filename="Terminal"
# Während des Scaffoldings
npx create-twenty-app@latest my-app --port 3000
# Oder nach dem Scaffolding
yarn twenty remote add --local --port 3000
```
Von hier aus können Sie:
```bash filename="Terminal"
# Eine neue Entität zu Ihrer Anwendung hinzufügen (geführt)
# Add a new entity to your application (guided)
yarn twenty entity:add
# Die Funktionsprotokolle Ihrer Anwendung überwachen
# Watch your application's function logs
yarn twenty function:logs
# Eine Funktion anhand ihres Namens ausführen
# Execute a function by name
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Die Pre-Installationsfunktion ausführen
# Execute the pre-install function
yarn twenty function:execute --preInstall
# Die Post-Installationsfunktion ausführen
# Execute the post-install function
yarn twenty function:execute --postInstall
# Die Anwendung für die Verteilung erstellen
yarn twenty app:build
# Build the app for distribution
yarn twenty build
# Die Anwendung auf npm oder einen Twenty-Server veröffentlichen
yarn twenty app:publish
# Publish the app to npm or a Twenty server
yarn twenty publish
# Die Anwendung aus dem aktuellen Arbeitsbereich deinstallieren
yarn twenty app:uninstall
# Uninstall the application from the current workspace
yarn twenty uninstall
# Hilfe zu Befehlen anzeigen
# Display commands' help
yarn twenty help
```
@@ -177,7 +221,7 @@ export default defineObject({
Spätere Befehle fügen weitere Dateien und Ordner hinzu:
* `yarn twenty app:dev` generiert automatisch zwei typisierte API-Clients in `node_modules/twenty-sdk/clients`: `CoreApiClient` (für Arbeitsbereichsdaten über `/graphql`) und `MetadataApiClient` (für Arbeitsbereichskonfiguration und Datei-Uploads über `/metadata`).
* `yarn twenty dev` will auto-generate two typed API clients in `node_modules/twenty-sdk/clients`: `CoreApiClient` (for workspace data via `/graphql`) and `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`).
* `yarn twenty entity:add` fügt unter `src/` Entitätsdefinitionsdateien für Ihre benutzerdefinierten Objekte, Funktionen, Frontend-Komponenten, Rollen, Skills und mehr hinzu.
## Authentifizierung
@@ -836,7 +880,7 @@ Hauptpunkte:
* Frontend-Komponenten sind React-Komponenten, die in isolierten Kontexten innerhalb von Twenty gerendert werden.
* Das Feld `component` verweist auf Ihre React-Komponente.
* Komponenten werden während `yarn twenty app:dev` automatisch gebaut und synchronisiert.
* Components are built and synced automatically during `yarn twenty dev`.
Sie können neue Frontend-Komponenten auf zwei Arten erstellen:
@@ -1164,7 +1208,7 @@ Sie können neue Agenten auf zwei Arten erstellen:
### Generierte typisierte Clients
Zwei typisierte Clients werden von `yarn twenty app:dev` automatisch generiert und basierend auf Ihrem Arbeitsbereichs-Schema in `node_modules/twenty-sdk/clients` gespeichert:
Two typed clients are auto-generated by `yarn twenty dev` and stored in `node_modules/twenty-sdk/clients` based on your workspace schema:
* **`CoreApiClient`** — fragt den `/graphql`-Endpunkt nach Arbeitsbereichsdaten ab
* **`MetadataApiClient`** — ruft über den Endpunkt `/metadata` die Arbeitsbereichskonfiguration und Datei-Uploads ab.
@@ -1180,7 +1224,7 @@ const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
```
`CoreApiClient` wird von `yarn twenty app:dev` automatisch neu generiert, sobald sich Ihre Objekte oder Felder ändern. `MetadataApiClient` ist im SDK bereits enthalten.
`CoreApiClient` is re-generated automatically by `yarn twenty dev` whenever your objects or fields change. `MetadataApiClient` ist im SDK bereits enthalten.
#### Laufzeit-Anmeldedaten in Logikfunktionen
@@ -1251,11 +1295,11 @@ Ein minimales End-to-End-Beispiel, das Objekte, Logikfunktionen, Frontend-Kompon
Sobald Sie Ihre App mit `app:dev` entwickelt haben, verwenden Sie `app:build`, um sie in ein verteilbares Paket zu kompilieren.
```bash filename="Terminal"
# Die App erstellen (Ausgabe nach .twenty/output/)
yarn twenty app:build
# Build the app (output goes to .twenty/output/)
yarn twenty build
# Build ausführen und ein Tarball (.tgz) für die Verteilung erstellen
yarn twenty app:build --tarball
# Build and create a tarball (.tgz) for distribution
yarn twenty build --tarball
```
Der Build-Prozess:
@@ -1295,10 +1339,10 @@ Verwenden Sie `app:publish`, um Ihre App zu verteilen — entweder zur npm-Regis
```bash filename="Terminal"
# Publish to npm (requires npm login)
yarn twenty app:publish
yarn twenty publish
# Publish with a dist-tag (e.g. beta, next)
yarn twenty app:publish --tag beta
yarn twenty publish --tag beta
```
Dies baut die App und führt `npm publish` aus dem Verzeichnis `.twenty/output/` aus. Das veröffentlichte Paket kann dann von jedem Arbeitsbereich über den Twenty-Marktplatz installiert werden.
@@ -1307,7 +1351,7 @@ Dies baut die App und führt `npm publish` aus dem Verzeichnis `.twenty/output/`
```bash filename="Terminal"
# Publish directly to a Twenty server
yarn twenty app:publish --server https://app.twenty.com
yarn twenty publish --server https://app.twenty.com
```
Dies erstellt beim Build einen Tarball, lädt ihn über die GraphQL-Mutation `uploadAppTarball` auf den Server hoch und stößt die Installation in einem Schritt an. Dies ist nützlich für private Bereitstellungen oder Tests gegen einen bestimmten Server.
@@ -1371,13 +1415,13 @@ Fügen Sie dann ein `twenty`-Skript hinzu:
}
```
Jetzt können Sie alle Befehle über `yarn twenty <command>` ausführen, z. B. `yarn twenty app:dev`, `yarn twenty help` usw.
Now you can run all commands via `yarn twenty <command>`, e.g. `yarn twenty dev`, `yarn twenty help`, etc.
## Fehlerbehebung
* Authentifizierungsfehler: Führen Sie `yarn twenty auth:login` aus und stellen Sie sicher, dass Ihr API-Schlüssel die erforderlichen Berechtigungen hat.
* Verbindung zum Server nicht möglich: Überprüfen Sie die API-URL und dass der Twenty-Server erreichbar ist.
* Typen oder Client fehlen/veraltet: Starten Sie `yarn twenty app:dev` neu — der typisierte Client wird automatisch generiert.
* Dev-Modus synchronisiert nicht: Stellen Sie sicher, dass `yarn twenty app:dev` läuft und dass Änderungen von Ihrer Umgebung nicht ignoriert werden.
* Types or client missing/outdated: restart `yarn twenty dev` — it auto-generates the typed client.
* Dev mode not syncing: ensure `yarn twenty dev` is running and that changes are not ignored by your environment.
Discord-Hilfekanal: https://discord.com/channels/1130383047699738754/1130386664812982322

Some files were not shown because too many files have changed in this diff Show More