Compare commits

...
Author SHA1 Message Date
Charles BochetandCursor b2f0064dd7 fix: nullify foreign keys on soft-delete to prevent misleading "Not shared" display
When a record is soft-deleted, FK references from other records were left
dangling, causing the frontend to show "Not shared" (ForbiddenFieldDisplay)
instead of an empty cell. This mimics PostgreSQL's ON DELETE SET NULL
constraint behavior at the application level for soft-deletes.

Closes #20076

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 23:21:59 +02:00
01b4754e62 New name not appearing when renaming "Stages" in data-model settings (#20246)
## Summary
- Resolve standard field `label`, `description`, and `icon` overrides
through dedicated GraphQL field resolvers.
- Fall back to the source locale safely when the request locale is
missing, and allow direct overrides to apply for non-source locales when
translations are absent.
- Enrich metadata subscription payloads for both `before` and `after`,
reusing the same override application path for field and object
metadata.
- Update and extend tests to cover the revised override behavior.

## Testing
- Updated unit coverage for standard override resolution, including the
non-source-locale fallback path.
- Not run (not requested).

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 20:16:00 +00:00
e6399b180e Fix/workspace member avatars 20193 (#20200)
Fixes #20193

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

**Root Cause:**

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

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

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

**Changes Made:**

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

**Testing:**

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

Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 19:42:12 +00:00
8c2885f9ed i18n - docs translations (#20248)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-04 20:52:46 +02:00
a76047f28b i18n - docs translations (#20243)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-04 18:53:30 +02:00
Sri Hari Haran SharmaandGitHub 281eaa3721 fix rest filter default conjunction detection (#20133)
Fixes #20128 

## Summary

Fix REST API filter parsing when bare filters are mixed with explicit
conjunctions.

## What changed

- Replaced the loose parentheses check in
`addDefaultConjunctionIfMissing` with proper root conjunction detection.
- Shared the root conjunction regex with `parseFilter`.
- Added regression tests for mixed filters like
`status[eq]:'TODO',and(title[ilike]:'%test%')`.

## Validation

- `npx nx test twenty-server
--testPathPatterns=add-default-conjunction.util.spec.ts --runInBand
--coverage=false`
- `npx prettier --check ...`
2026-05-04 16:18:56 +00:00
martmullandGitHub c804f27846 Add check for manifest uuid version (#20239)
As title

<img width="1059" height="203" alt="image"
src="https://github.com/user-attachments/assets/c6840c4e-792b-45da-b450-addd77af0de7"
/>
2026-05-04 16:06:50 +00:00
martmullandGitHub 54e22423df Improve twenty deploy cli logs (#20237)
## Before
<img width="1074" height="562" alt="image"
src="https://github.com/user-attachments/assets/a2fbe902-d34e-40e4-87c9-f344a06fd6ae"
/>

## After

<img width="1107" height="605" alt="image"
src="https://github.com/user-attachments/assets/af78276a-f4c7-42f9-9347-01d562b1a779"
/>
2026-05-04 15:25:49 +00:00
a0dd7d9e22 i18n - translations (#20240)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-04 17:31:07 +02:00
97ec720d2c fix: show active advanced filter count badge in dropdown button (#20229)
## Summary

Replaces the hardcoded `0` in
`ViewBarFilterDropdownAdvancedFilterButton` with the actual count of
active advanced filter rules, matching the behavior of
`AdvancedFilterChip` in the view bar.

## What changed

In
`packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownAdvancedFilterButton.tsx`:

- Imported `useAtomComponentSelectorValue` and
`rootLevelRecordFilterGroupComponentSelector`
- Imported `useChildRecordFiltersAndRecordFilterGroups`
- Replaced `const advancedFilterQuerySubFilterCount = 0; // TODO` with
the real computed count via the same hook pattern used in
`AdvancedFilterChip.tsx`

The pill badge will now appear on the "Advanced filter" dropdown menu
item showing the number of active advanced filter rules (e.g. "2" when
two rules are active).

## References

- Fixes #20207

---------

Co-authored-by: wadeKeith <wade@twenty.app>
Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 15:08:30 +00:00
Thomas TrompetteandGitHub 95a34d8517 Return false instead of throwing when event stream does not exist (#20165)
## Summary

- When an event stream expires (TTL), `addQueryToEventStream` and
`removeQueryFromEventStream` now return `false` instead of throwing
`EVENT_STREAM_DOES_NOT_EXIST` as an `InternalServerError`
- Frontend checks the mutation return value and triggers the
destroy/recreate cycle, same recovery behavior without the error path
- Removes `EVENT_STREAM_DOES_NOT_EXIST` from exception code, exception
filter, and frontend graceful error check since it's no longer thrown

## Test plan

- [x] Verify that when an event stream TTL expires, the frontend
silently recreates the stream without error noise in logs/Sentry
- [x] Verify that `NOT_AUTHORIZED` errors still throw correctly on both
mutations
- [ ] Verify that the subscription `onEventSubscription` still works
end-to-end with stream creation, query registration, and heartbeat TTL
refresh

Made with [Cursor](https://cursor.com)
2026-05-04 15:06:18 +00:00
MarieandGitHub 4852ac401a Add server upgrade status on admin panel (#20107)
## Summary

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

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


## Computed metrics

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

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

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

## Fetching strategy

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

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

## Introduced cache keys

All under the `EngineHealth` cache-storage namespace:

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

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

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

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

```sql
CREATE INDEX "IDX_upgradeMigration_workspaceId_name_attempt"
  ON "core"."upgradeMigration" ("workspaceId", "name", "attempt")
  WHERE "workspaceId" IS NOT NULL;
2026-05-04 15:06:07 +00:00
1cd983a330 fix: handle missing file entity in avatar deletion listener (#20192)
## Problem
When a `workspaceMember` is updated (e.g., theme/locale/avatar changes),
the `WorkspaceMemberAvatarFileDeletionListener` triggers file deletion.
If the referenced file entity doesn't exist in the database, an
unhandled `EntityNotFoundError` crashes the NestJS server, causing a 502
loop.

## Change
Wrap the file deletion call in a try-catch that gracefully handles
`EntityNotFoundError` as a no-op — if the file doesn't exist, there's
nothing to delete.

Fixes #20191.

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

Co-authored-by: martmull <martmull@hotmail.fr>
2026-05-04 14:58:56 +00:00
37ca09e8f9 i18n - docs translations (#20238)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-04 17:06:43 +02:00
91124a3cb8 AI - Add azure foundry provider (#20170)
[Merge this before](https://github.com/twentyhq/twenty-infra/pull/655)

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-05-04 14:45:06 +00:00
Paul RastoinandGitHub 41ad63a8ab [DockerFile] Optimize twenty-server deps and build (#20132)
# Introduction

Aiming for faster cd process

## Splitting front end server deps
Reduce dependencies bloating when target is server only, installing only
root repo dev deps and server dev and prod deps

Still pruning before copying to prod node_modules

## Server only remove twenty-ui

Also removing twenty-ui from server build as it was not consumed at all

Depends on https://github.com/twentyhq/twenty/pull/20140
2026-05-04 14:24:52 +00:00
9ddf9af4c4 i18n - translations (#20236)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-04 16:18:30 +02:00
3ffda0a29e Add twenty version validation (#20227)
as title, server version is checked before app deploy, and app install
commands

### New section in publishing doc
<img width="1344" height="912" alt="image"
src="https://github.com/user-attachments/assets/2a9335e7-0a7a-4973-a2db-f30f03181001"
/>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-04 13:55:31 +00:00
WeikoandGitHub 0bb345b75f Fix empty record page on system objects for non-English workspace members (#20235)
## Context
Since #19890 (Translate standard page layouts), the server's
`PageLayoutTab.title` resolver translates standard tab titles at query
time. A workspace member in French viewing a `messageChannel` (or any
system object with a standard page layout) receives `tab.title =
"Accueil"` / `"Chronologie"` instead of `"Home"` / `"Timeline"`.

The `SYSTEM_OBJECT_TABS` guard in `PageLayoutTabsRenderer` was comparing
against an English-only literal allow-list, so every tab was dropped,
`sortedTabs` became empty, and `<PageLayoutMainContent />` never mounted
— the record page rendered blank (no fields, timeline, email thread,
etc.).

## Fix
Only run the allow-list filter when the resolved layout is the synthetic
`DEFAULT_RECORD_PAGE_LAYOUT` (the client-side fallback for the few
system objects with no server-side standard page layout config, e.g.
`workspaceMember`, `attachment`, `message`). That layout ships hardcoded
English tabs, so the English allow-list still works in every locale.

System objects that do have a server-side standard page layout
(`messageChannel`, `connectedAccount`, `workflowRun`, …) are no longer
filtered at all, the server only ever persists Home/Timeline/Flow tabs
for them, so no filter is needed.

## Before
<img width="1262" height="722" alt="Screenshot 2026-05-04 at 15 15 54"
src="https://github.com/user-attachments/assets/348c9e9d-0ae1-4046-8ead-470ed8263cb5"
/>


## After
<img width="1281" height="658" alt="Screenshot 2026-05-04 at 15 15 36"
src="https://github.com/user-attachments/assets/7a9953b1-7320-4f1a-8c02-1688d3eda3ae"
/>


## Note
Next step should be to backfill those system objects with real record
page layouts so we can remove this filter logic
2026-05-04 13:36:36 +00:00
3c7c62c79f fix(server): deduplicate @opentelemetry/api to fix NoopMeterProvider (#20231)
## Summary

**All OTel metrics in twenty-server have been silently dropped since
April 30.**

### Root cause

PR #20149 (`bump @sentry/profiling-node 10.27→10.51`) pulled in
`@sentry/node@10.51.0`, which declares `@opentelemetry/api: ^1.9.1` as a
**dependency** (not peer). Yarn installed it as a **nested** copy at
`1.9.1`, while the hoisted copy stayed at `1.9.0`.

At startup in `instrument.ts`:
1. `Sentry.init()` uses the **nested `1.9.1`** to register `trace`,
`propagation`, `context` on the OTel global → global version becomes
**`1.9.1`**
2. `setGlobalMeterProvider()` uses the **hoisted `1.9.0`** →
`registerGlobal` sees version mismatch (`1.9.1` ≠ `1.9.0`) → **silently
returns `false`**
3. Global stays `NoopMeterProvider` → every counter, gauge, and
histogram in the server is a no-op

### What this PR does

1. **Reverts three troubleshooting PRs** that are no longer needed now
that the root cause is identified:
   - #20230 — heartbeat gauge
   - #20228 — OTLP export lifecycle logs
- #20221 — Sentry revert to 10.27 (which never actually downgraded in
`yarn.lock` since `^10.27.0` resolved to `10.51.0`)

2. **Fixes the root cause**:
- Root Yarn resolution pinning `@opentelemetry/api` to `1.9.1` → single
copy in the entire tree, Sentry and Twenty share the same instance
- Named import in `instrument.ts` (`import { metrics as otelMetrics }`
instead of default import) as defense-in-depth against CJS interop
issues

### Verified on dev cluster

Exec'd into the running pod and confirmed:
- `@sentry/node` nests `@opentelemetry/api@1.9.1`, hoisted is `1.9.0`
- `Sentry.init()` → global version `1.9.1` → `setGlobalMeterProvider`
with VERSION `1.9.0` → returns `false` → `NoopMeterProvider`
- Same-version registration returns `true` → `MeterProvider` ✓

## Test plan
- [ ] CI passes (lint, typecheck, build)
- [ ] Deploy to dev cluster and verify metrics flow to collector
- [ ] Confirm `node_modules/@opentelemetry/api/package.json` shows
`1.9.1` with no nested copy under `@sentry/`

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 15:15:00 +02:00
Paul RastoinandGitHub 596ce32bd6 Fix front unit test on main (#20233)
Jest mocks runs before the const is defined
2026-05-04 15:14:43 +02:00
d2cfbf319b [Website] Implement translations. (#20171)
As title.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 10:41:46 +00:00
a3cf565ba3 feat(server): add heartbeat gauge + first-export-attempt log for OTLP (#20230)
## Summary
- Adds a trivial always-on observable gauge (`twenty.heartbeat = 1`) so
the OTLP exporter fires on every 10s collection tick, even on idle pods.
Without this, `PeriodicExportingMetricReader` skips the export when
`scopeMetrics` is empty, so the process-log wrapper never runs and we
can't prove OTLP connectivity.
- Adds a one-time "first periodic export attempt" log line inside the
wrapped exporter, completing the startup log sequence: `OTLP reader
enabled` → `first periodic export attempt` → `first export ok` / `export
failed`.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 12:30:59 +02:00
Abdul RahmanandGitHub fa8304d323 Fix record table dashboard save (#20202)
## Summary

Fixes `METADATA_VALIDATION_FAILED` / “view field already exists” when
saving dashboard **record table** widgets after the first persist (e.g.
several table widgets on one dashboard).

## Problem

Draft view columns used **client-generated** `viewField` ids. After
save, the API stored **different** ids. The next upsert still sent the
old draft ids as `viewFieldId`. The server only matched on that id,
missed every row, and tried to **create** columns that already existed
for the same `fieldMetadataId` + view.
2026-05-04 09:57:21 +00:00
467ddaa27a feat(server): OTLP metrics export logs for troubleshooting (#20228)
## Summary

Adds **grep-friendly** `console` logging around the OpenTelemetry
metrics OTLP exporter in
[`packages/twenty-server/src/instrument.ts`](packages/twenty-server/src/instrument.ts)
so production / staging can confirm whether the app is exporting metrics
and why exports fail.

## Log format

- Prefix: **`[Twenty OTEL metrics]`** (easy to filter in Loki / `kubectl
logs | grep`).
- **Startup:** whether the OTLP reader is enabled, `exportIntervalMs`,
and endpoint as `protocol//host/path` only (no credentials).
- **First successful export:** one `console.log` per process (`first
export ok`) with metric data point count — avoids spamming every 10s.
- **Each failed export:** `console.warn` with result code, point count,
and serialized error (including nested `AggregateError` causes when
present).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 11:44:51 +02:00
fc4cf7fe09 i18n - translations (#20226)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-04 11:34:51 +02:00
9e94045fa5 feat(apps): generic OAuth provider support for app SDK (#20181)
## Summary

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

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

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

## Architecture

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

## Reference app

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

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

## Tests

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

## Test plan

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

## Out of scope (deliberately)

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

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-04 11:26:34 +02:00
ff22988caf revert: Sentry #20064 + @sentry 10.27 (prod bisect) (#20221)
## Summary

Reverts **#20064** (`feat(sentry): propagate workspace context to all
spans`) and downgrades **@sentry** packages from **10.51** back to
**10.27** (reversing **#20149**), to validate in production whether
recent Sentry/instrumentation changes correlate with OTLP/metrics
issues.

## Changes

1. **Revert #20064** — removes `beforeSendSpan` from `instrument.ts`,
restores `WorkspaceAuthContextMiddleware` / `BullMQDriver` behavior, and
deletes the three `apply-workspace-sentry-*` utils added in that PR.
2. **Sentry versions** — `packages/twenty-server` (`@sentry/nestjs`,
`@sentry/node`, `@sentry/profiling-node`) and `packages/twenty-front`
(`@sentry/react`) set to `^10.27.0`; `yarn.lock` regenerated via `yarn
install`.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 11:09:48 +02:00
Paul RastoinandGitHub 74cc2b4c87 Twenty email deps (#20223) 2026-05-04 11:09:34 +02:00
Charles BochetandGitHub 7aa2afc67e fix(shared): add uuid, @types/uuid, @types/qs for Docker CD (#20222)
## Summary

- `twenty-shared` imports `uuid` (in `actor.composite-type.ts` and
`createAnyFieldRecordFilterBaseProperties.ts`) and `qs` (in
`getAppPath.ts`, `getSettingsPath.ts`), but `uuid` was not declared in
`twenty-shared/package.json` and `@types/uuid` / `@types/qs` were
missing as devDependencies.
- After scoped/hoisted deps (#20140) those types/runtime came from the
root `package.json` and are no longer guaranteed in the Docker
`common-deps` graph, so `twenty-shared:build` (pulled in before
`twenty-website-new` build) fails with `TS7016: Could not find a
declaration file for module 'uuid' / 'qs'` in the CD pipeline (see
[twenty-infra run
25309442711](https://github.com/twentyhq/twenty-infra/actions/runs/25309442711)).
- Same shape of fix as #20219 which added `@types/lodash.camelcase`.

## Test plan

- [x] `npx nx build twenty-shared` succeeds locally
- [ ] CD pipeline succeeds for `Build website-new`
2026-05-04 11:03:23 +02:00
a025dc368b fix(shared): @types/lodash.camelcase for Docker CD (#20219)
Adds `@types/lodash.camelcase` to `twenty-shared`.

**Why:** `lodash.camelcase` has no bundled types. Those types used to
come from the root `devDependencies`; after scoped/hoisted deps
(#20140), they are no longer guaranteed in the Docker `common-deps`
graph, so `twenty-shared:build` (pulled in before server Lingui) fails
with TS7016. Declaring the types on the package that imports
`lodash.camelcase` fixes CD.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 10:40:27 +02:00
5bf7e8b101 test(upgrade): assert sequence-runner error structurally instead of snapshot (#20213)
## Summary

The integration test `should throw when cursor command is not found in
the sequence` in `failing-sequence-runner.integration-spec.ts` used
`toThrowErrorMatchingSnapshot()`. The captured snapshot included the
literal `TWENTY_CROSS_UPGRADE_SUPPORTED_VERSIONS` list from
`upgrade-sequence-reader.service.ts`, which grows by one entry on every
Twenty release. As a result, the snapshot drifted and broke whenever a
new instance command landed (noticed during PR #20181), creating
recurring "snapshot needs updating" churn with no real signal value.

This PR replaces the snapshot assertion with a regex match on the
structural part of the error message:

```ts
).rejects.toThrow(/Step "RemovedCommand" not found in upgrade sequence/);
```

The regex still catches the same class of regressions (the runner
failing to surface a missing-step error) without pinning the version
list. The now-empty snap file is removed (it had only this one entry).

## Test plan

- [ ] CI integration tests pass on this branch
- [ ] No remaining references to the deleted snapshot

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 21:52:27 +02:00
276d4f6e84 fix(code-interpreter): three correctness fixes for the TwentyMCP helper + tool output shape (#20103)
## Summary

Three independently-useful correctness fixes for the `code_interpreter`
tool, all surfaced while standing up a self-hosted code interpreter
against MCP. Each is isolated to one bug and applies regardless of
`CODE_INTERPRETER_TYPE`.

### 1. UI: unwrap `execute_tool` envelope when rendering
`code_interpreter` output

When `code_interpreter` is invoked through MCP's `execute_tool`
meta-tool, the result arrives wrapped: `{success, result: {stdout,
exitCode, files, ...}, ...}`. `ToolStepRenderer` reads `exitCode` at the
top level, which is `undefined` → the step renders as "Failed" even on a
clean `exitCode === 0`. Symmetric to the input-side unwrap that already
exists; the fix lifts `outputObj.result` when `rawToolName ===
'execute_tool'`.

### 2. Helper: route `TwentyMCP.call_tool` through `execute_tool` for
catalog tools

The `TwentyMCP` helper injected into every code-interpreter sandbox
exposes a `call_tool(name, arguments)` method. Direct MCP calls only
work for the 5 meta-tools (`get_tool_catalog`, `learn_tools`,
`execute_tool`, `load_skills`, `search_help_center`); the 250+ catalog
tools are accessed through `execute_tool`. Today
`twenty.call_tool('find_companies', {...})` raises "Unknown tool". This
commit detects catalog tools and auto-routes them through
`execute_tool`. It also flattens the nested `{catalog: {category:
[...]}}` shape returned by `list_tools()` and propagates `{success:
false}` envelopes as explicit exceptions (they were being silently
returned as dicts).

### 3. Prompt: stop the agent from hallucinating \`import twenty\`

Despite the helper being pre-injected, models frequently emitted
\`import twenty\` and crashed with \`ModuleNotFoundError\`. Two
contributing sources: the helper docstring did not explicitly say "do
not import," and the code-interpreter skill template's example block
referenced placeholder tool names. Fix: explicit "DO NOT import twenty"
block in the helper + rewritten skill examples using real tool names and
real response shapes.

## Test plan

- [ ] Existing \`code_interpreter\` tests still pass.
- [ ] Run a chat turn that invokes \`code_interpreter\` indirectly via
MCP \`execute_tool\` and confirm the UI no longer flips to "Failed" on
\`exitCode === 0\`.
- [ ] From inside the sandbox, run \`twenty.call_tool('find_companies',
{limit: 5})\` and confirm records return (was raising "Unknown tool").
- [ ] Confirm \`twenty.list_tools()\` returns a flat list, not the
nested \`{catalog: {...}}\` envelope.
- [ ] Trigger a tool error from inside the sandbox and confirm it raises
rather than returning a \`{success: false}\` dict.
- [ ] Ask an LLM to use \`code_interpreter\`; confirm it does not emit
\`import twenty\`.

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-05-03 11:16:53 +02:00
nitinandGitHub 4f439bbe43 [AI] Prefer batch tools in system prompts (#20173) 2026-05-01 14:55:15 +02:00
4fbd8b207d chore: sync AI model catalog from models.dev (#20178)
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-05-01 08:50:48 +02:00
f7d812fca6 fix: disable sync on seeded message and calendar channels (#20168)
## Summary

The dev seeder creates `ConnectedAccount` records with fake OAuth tokens
(`'exampleRefreshToken'` / `'exampleAccessToken'`) and points
`MessageChannel` / `CalendarChannel` records at them with
`isSyncEnabled: true`. When the sync cron jobs run in the demo
workspace, they:

1. Pick up these seeded channels (filter is `isSyncEnabled: true` +
pending sync stage)
2. Try to refresh the fake OAuth tokens
3. Mark the channels as `FAILED_INSUFFICIENT_PERMISSIONS`
4. Surface a "Sync lost with mailbox X — please reconnect" banner in the
UI

This banner appears every time the demo workspace is loaded, even though
nothing is actually broken.

## Fix

Set `isSyncEnabled: false` on all 12 seeded channels (6 message, 6
calendar). This is the canonical "don't sync this channel" mechanism —
the same state a real user lands in when they toggle sync off in account
settings.

## Why this approach

- **ConnectedAccount records stay**: the demo workspace still shows Tim,
Jony, Phil, Jane as having connected their email/calendar — realistic
- **Pre-seeded messages and calendar events stay visible**: those don't
depend on `isSyncEnabled`
- **Crons no longer pick them up**: they filter on `isSyncEnabled:
true`, so `false` short-circuits the entire sync attempt — no failure,
no banner
- **Semantically correct**: the seeded accounts have fake tokens that
were never going to sync successfully; `isSyncEnabled: true` was
effectively a lie
- **No production code touched**: no `isDemo` flags, no magic-string
detection, no workspace-ID filters in the cron path

## Alternatives considered and rejected

- **Add an `isDemo` flag**: schema change, leaks demo knowledge into
production tables
- **Skip channels with fake tokens (`example*` pattern)**: hacky
magic-string detection in the auth refresh path
- **Filter demo workspace IDs in the cron**: production paths shouldn't
reference demo IDs
- **Don't activate demo workspaces**: breaks the demo workspace UX
entirely

## Test plan

- [ ] Reset the database and reseed (`npx nx database:reset
twenty-server`)
- [ ] Load the demo workspace — confirm no "Sync lost with mailbox"
banner appears
- [ ] Confirm seeded connected accounts still show in Settings →
Accounts
- [ ] Confirm pre-seeded messages and calendar events still appear in
the UI
- [ ] Confirm a real connected account (added via OAuth) still syncs
normally — its channel will have `isSyncEnabled: true` and the cron will
pick it up

## Related

Companion to https://github.com/twentyhq/twenty/pull/20167, which
removes the `--dev-mode` cron filter that was masking this banner issue
in the dev image.

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

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

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

## What changed

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

## Why this is safe

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

## Context

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

## Test plan

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

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

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-01 07:51:21 +02:00
WeikoandGitHub 85752f8a61 Bump 2.3.0 (#20169) 2026-04-30 19:34:43 +02:00
8a0225e974 Dispatch root package.json hoisted deps and devDeps (#20140)
# Introduction
Dispatching root package.json devDeps, prod deps
Taking care of keeping non imported module used at build/ci level in the
root package.json

## Motivation
Avoid redundant deps declaration, better scoping allow better workspace
deps granularity installation.

<img width="385" height="247" alt="image"
src="https://github.com/user-attachments/assets/9d7162ec-ba01-4f58-8563-38333733fdf0"
/>

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-30 16:50:22 +00:00
abc67efd40 i18n - docs translations (#20166)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-30 18:50:57 +02:00
636deffb93 i18n - translations (#20164)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-30 18:01:45 +02:00
nitinandGitHub e1828b6f41 [AI] Add thread actions, filters, and archive support (#20068)
## PR Description

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

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


https://github.com/user-attachments/assets/1b179b7b-1a2a-4a7a-aa0a-c88f6f051a87
2026-04-30 15:42:10 +00:00
WeikoandGitHub 4b76457217 Select application excluding logo (#20159)
## Context
This is a temporary fix for cross-version upgrade process, a better fix
would be to expose an hasInstanceCommandBeenRun() util (and later a
decorator)
2026-04-30 15:13:54 +00:00
martmullandGitHub d8bd717f1f Update doc screenshots (#20160)
fixes
https://discord.com/channels/1130383047699738754/1496579088889024542
2026-04-30 14:54:49 +00:00
b44fb1ad23 fix(security): reject ?token= URL query parameter for authentication (#20154)
## Summary

Removes the `?token=` URL query-parameter fallback from JWT
authentication. Every authenticated route (`/graphql`, `/metadata`,
REST, etc.) used to accept a full workspace JWT in the URL alongside the
`Authorization` header. The fallback was intended for the REST API
Playground only, but it was wired into the global Passport JWT extractor
and applied to every route.

URL-borne tokens leak into:
- Server access logs (nginx / Apache / CDN / proxy / load balancer)
- Log aggregators (Datadog, CloudWatch, Loki, Sumo, …)
- Browser history (and synced across devices)
- `Referer` headers when navigating to external pages
- Browser extensions with `tabs`/`webNavigation` permissions

A leaked log line was equivalent to a leaked workspace credential for
the lifetime of the token.

## What changed

- **`jwt-wrapper.service.ts`** — `extractJwtFromRequest()` is now
header-only (`ExtractJwt.fromAuthHeaderAsBearerToken()`). No URL
fallback anywhere in the system.
- **`open-api.service.ts` / `base-schema.utils.ts`** — Dropped the
`token?: string` plumbing that propagated the URL token into the schema
description. The "Authentication" section gains a "Never put your token
in a URL" warning. The "Usage with LLMs" section is rewritten to point
at the **Twenty MCP server** (header-authenticated, exposes typed tools
— the right tool for AI agents) instead of telling users to paste
tokenized OpenAPI URLs into Cursor/ChatGPT.
- **`RestPlayground.tsx`** — Now fetches the OpenAPI schema with
`Authorization: Bearer ${playgroundApiKey}` and passes the JSON document
to Scalar via `spec.content` instead of constructing a URL with
`?token=`. Aborts in-flight fetches on unmount/key change.
- **New integration test** — Asserts `?token=` is rejected on `/rest/*`,
`/graphql`, `/metadata`, and that `/rest/open-api/core?token=` returns
the unauthenticated base schema (no workspace object paths).

## Why not keep `?token=` scoped to the OpenAPI endpoint only

The first instinct was to narrow the fallback to just
`/rest/open-api/*`, since that endpoint is what the Scalar playground
component fetches. But the same log-leakage attack still applies to that
endpoint — the workspace JWT would still sit in access logs, just from
one URL pattern instead of all of them. The cleaner long-term fix is to
remove the URL pattern entirely and let the playground fetch with a
header (Scalar supports `spec.content` natively). For LLM agent use, the
MCP server is a strictly better path — typed tools, OAuth or
header-based API key auth, no tokens in URLs anywhere.

## Not affected

File downloads at `file-url.service.ts` also use `?token=` URLs but with
separate, short-lived `FILE`-typed tokens validated by
`file-by-id.guard.ts` directly (not via `extractJwtFromRequest`). That
mechanism is scoped per-file with limited TTL and is acceptable.

## Action required for users

Anyone who previously pasted `?token=` URLs into LLM tools, scripts,
bookmarks, or shared configs should rotate their workspace API keys.
Those tokens are likely captured in server logs / chat histories
somewhere.

## Test plan

- [x] `npx nx typecheck twenty-server` — clean
- [x] `npx nx typecheck twenty-front` — clean
- [x] `npx nx lint:diff-with-main twenty-server` — clean
- [x] `npx nx lint:diff-with-main twenty-front` — clean
- [x] OpenAPI utils unit tests + snapshots — 11/11 pass
- [ ] Run the new integration test against a live server: `nx run
twenty-server:test:integration:with-db-reset` and verify
`url-token-auth-rejection.integration-spec.ts` passes
- [ ] Manually open Settings → Playground → REST, confirm the schema
loads (now via Bearer header instead of `?token=` URL)
- [ ] Manually verify `POST /metadata?token=<jwt>` (no Authorization
header) returns Forbidden, and the same request with the token in the
header returns the user

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-30 17:00:26 +02:00
f32b03a3ec i18n - docs translations (#20161)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-30 16:58:55 +02:00
martmullandGitHub c8e405cb4e Add twenty sdk server upgrade command (#20158)
##
The command pulls the image, compares it against the one the container
was created from, and only recreates the container if the image actually
changed. Your data volumes are preserved — only the container is
replaced.
2026-04-30 13:03:41 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Charles Bochet
83db37d33f chore(deps): bump @sentry/profiling-node from 10.27.0 to 10.51.0 (#20149)
Bumps
[@sentry/profiling-node](https://github.com/getsentry/sentry-javascript)
from 10.27.0 to 10.51.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/getsentry/sentry-javascript/releases"><code>@​sentry/profiling-node</code>'s
releases</a>.</em></p>
<blockquote>
<h2>10.51.0</h2>
<h3>Important Changes</h3>
<ul>
<li>
<p><strong>feat(cloudflare): Add trace propagation for RPC method calls
(<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/20343">#20343</a>)</strong></p>
<p>Trace context is now propagated across Cloudflare Workers RPC calls,
connecting traces between Workers and Durable Objects.
This feature is opt-in and requires setting
<code>enableRpcTracePropagation: true</code> in your SDK
configuration:</p>
<pre lang="ts"><code>// Worker
export default Sentry.withSentry(
  env =&gt; ({
    dsn: env.SENTRY_DSN,
    enableRpcTracePropagation: true,
  }),
  handler,
);
<p>// Durable Object<br />
export const MyDurableObject =
Sentry.instrumentDurableObjectWithSentry(<br />
env =&gt; ({<br />
dsn: env.SENTRY_DSN,<br />
enableRpcTracePropagation: true,<br />
}),<br />
MyDurableObjectBase,<br />
);<br />
</code></pre></p>
</li>
<li>
<p><strong>feat(hono)!: Change setup for <code>@sentry/hono/node</code>
(<code>init</code> in external file) (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/20497">#20497</a>)</strong></p>
<p>To improve Node.js instrumentation, the <code>sentry()</code>
middleware exported from <code>@sentry/hono/node</code> no longer
accepts configuration options.
Instead, you must configure the SDK by calling
<code>Sentry.init()</code> in a dedicated instrumentation file that runs
before your application code (read more in the <a
href="https://github.com/getsentry/sentry-javascript/blob/develop/packages/hono/README.md">Hono
SDK readme</a>:</p>
<pre lang="ts"><code>// instrument.mjs (or instrument.ts)
import * as Sentry from '@sentry/hono/node';
<p>Sentry.init({<br />
dsn: '<strong>DSN</strong>',<br />
tracesSampleRate: 1.0,<br />
});<br />
</code></pre></p>
</li>
<li>
<p><strong>feat(nitro): Add <code>@sentry/nitro</code> SDK (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/19224">#19224</a>)</strong></p>
<p>A new <code>@sentry/nitro</code> package provides first-class Sentry
support for <a href="https://nitro.build/">Nitro</a> applications, with
HTTP handler and error instrumentation, middleware tracing, request
isolation, and build-time source map uploading via
<code>withSentryConfig</code>.
Read more in the <a
href="https://docs.sentry.io/platforms/javascript/guides/nitro/">Nitro
SDK docs</a> and the <a
href="https://github.com/getsentry/sentry-javascript/blob/develop/packages/nitro/README.md">Nitro
SDK readme</a>.</p>
</li>
</ul>
<h3>Other Changes</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md"><code>@​sentry/profiling-node</code>'s
changelog</a>.</em></p>
<blockquote>
<h2>10.51.0</h2>
<h3>Important Changes</h3>
<ul>
<li>
<p><strong>feat(cloudflare): Add trace propagation for RPC method calls
(<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/20343">#20343</a>)</strong></p>
<p>Trace context is now propagated across Cloudflare Workers RPC calls,
connecting traces between Workers and Durable Objects.
This feature is opt-in and requires setting
<code>enableRpcTracePropagation: true</code> in your SDK
configuration:</p>
<pre lang="ts"><code>// Worker
export default Sentry.withSentry(
  env =&gt; ({
    dsn: env.SENTRY_DSN,
    enableRpcTracePropagation: true,
  }),
  handler,
);
<p>// Durable Object<br />
export const MyDurableObject =
Sentry.instrumentDurableObjectWithSentry(<br />
env =&gt; ({<br />
dsn: env.SENTRY_DSN,<br />
enableRpcTracePropagation: true,<br />
}),<br />
MyDurableObjectBase,<br />
);<br />
</code></pre></p>
</li>
<li>
<p><strong>feat(hono)!: Change setup for <code>@sentry/hono/node</code>
(<code>init</code> in external file) (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/20497">#20497</a>)</strong></p>
<p>To improve Node.js instrumentation, the <code>sentry()</code>
middleware exported from <code>@sentry/hono/node</code> no longer
accepts configuration options.
Instead, you must configure the SDK by calling
<code>Sentry.init()</code> in a dedicated instrumentation file that runs
before your application code (read more in the <a
href="https://github.com/getsentry/sentry-javascript/blob/develop/packages/hono/README.md">Hono
SDK readme</a>:</p>
<pre lang="ts"><code>// instrument.mjs (or instrument.ts)
import * as Sentry from '@sentry/hono/node';
<p>Sentry.init({<br />
dsn: '<strong>DSN</strong>',<br />
tracesSampleRate: 1.0,<br />
});<br />
</code></pre></p>
</li>
<li>
<p><strong>feat(nitro): Add <code>@sentry/nitro</code> SDK (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/19224">#19224</a>)</strong></p>
<p>A new <code>@sentry/nitro</code> package provides first-class Sentry
support for <a href="https://nitro.build/">Nitro</a> applications, with
HTTP handler and error instrumentation, middleware tracing, request
isolation, and build-time source map uploading via
<code>withSentryConfig</code>.
Read more in the <a
href="https://docs.sentry.io/platforms/javascript/guides/nitro/">Nitro
SDK docs</a> and the <a
href="https://github.com/getsentry/sentry-javascript/blob/develop/packages/nitro/README.md">Nitro
SDK readme</a>.</p>
</li>
</ul>
<h3>Other Changes</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/dc0b839ff4896cf90a02f5c1a6de54a31302dcf3"><code>dc0b839</code></a>
release: 10.51.0</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/b3cabee9a9348b9e67332262d44d3d1900424199"><code>b3cabee</code></a>
Merge pull request <a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/20599">#20599</a>
from getsentry/prepare-release/10.51.0</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/3be99a9afa77e49578e6839e4b32f97fb04fb0f8"><code>3be99a9</code></a>
meta(changelog): Update changelog for 10.51.0</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/bea1aad42277db894d5a299bfec3cdd633d6baf0"><code>bea1aad</code></a>
test(browser): Unflake some more tests (<a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/20591">#20591</a>)</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/50aa0859b3a188d34d0317dab3ad57f2140f02fe"><code>50aa085</code></a>
test(node): Unflake postgres tests (<a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/20593">#20593</a>)</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/1166839112c4766f210124dc0486ebbfd6db104b"><code>1166839</code></a>
fix(hono): Distinguish <code>.use()</code> middleware in sub-apps from
<code>.all()</code> handlers...</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/217ad4a69554281806eccbfeac1b27c4f43f6ffa"><code>217ad4a</code></a>
test(node): Fix flaky ANR test (<a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/20592">#20592</a>)</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/91ffb3fac90835ab160f8152527a54a5d64f3250"><code>91ffb3f</code></a>
test(node): Fix flaky worker thread integration test (<a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/20588">#20588</a>)</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/c4e3902c9297147158e730f017aba96e83ef619e"><code>c4e3902</code></a>
chore(ci): Do not report flaky test issues if we cannot find a test name
(<a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/20">#20</a>...</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/c0005cd387f3a7ea6fbb2e85041562c7f32e0484"><code>c0005cd</code></a>
test(node): Update timeout for cron integration tests (<a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/20586">#20586</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/getsentry/sentry-javascript/compare/10.27.0...10.51.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@sentry/profiling-node&package-manager=npm_and_yarn&previous-version=10.27.0&new-version=10.51.0)](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 <charles@twenty.com>
2026-04-30 12:02:34 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Charles Bochet
5bdbfe651e chore(deps): bump postal-mime from 2.6.1 to 2.7.4 (#20150)
Bumps [postal-mime](https://github.com/postalsys/postal-mime) from 2.6.1
to 2.7.4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/postalsys/postal-mime/releases">postal-mime's
releases</a>.</em></p>
<blockquote>
<h2>v2.7.4</h2>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.3...v2.7.4">2.7.4</a>
(2026-03-17)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>add missing originalKey to Header type and Uint8Array to Attachment
content (<a
href="https://github.com/postalsys/postal-mime/commit/92cc91c1c8477e0462cb0e93ddf8ea6aec6534d0">92cc91c</a>)</li>
<li>include originalKey in parsed headers output (<a
href="https://github.com/postalsys/postal-mime/commit/83521c87f62e5e095ae09913c70798f20e2ab347">83521c8</a>)</li>
<li>preserve __esModule and .default in CJS build for bundler interop
(<a
href="https://github.com/postalsys/postal-mime/commit/1466910e31608b9e5307724ecc6a0a3a70556048">1466910</a>)</li>
<li>prevent RFC 2047 encoded-word address fabrication (<a
href="https://github.com/postalsys/postal-mime/commit/844f92023d49d819ef13b9ad5c50b7c346eb02d3">844f920</a>)</li>
</ul>
<h2>v2.7.3</h2>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.2...v2.7.3">2.7.3</a>
(2026-01-09)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>correct TypeScript type definitions to match implementation (<a
href="https://github.com/postalsys/postal-mime/commit/b225d7cca422cb9bc3ab5301e94c4c0bef9a69e2">b225d7c</a>)</li>
</ul>
<h2>v2.7.2</h2>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.1...v2.7.2">2.7.2</a>
(2026-01-08)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>add null checks for contentType.parsed access (<a
href="https://github.com/postalsys/postal-mime/commit/ad8f4c62e0972fd0244859ee5a5184b2cac26395">ad8f4c6</a>)</li>
<li>improve RFC compliance for MIME parsing (<a
href="https://github.com/postalsys/postal-mime/commit/e004c3acb29d72ed7eaf1b0b66351cf8b82b970d">e004c3a</a>)</li>
</ul>
<h2>v2.7.1</h2>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.0...v2.7.1">2.7.1</a>
(2025-12-22)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>Add null checks for contentDisposition.parsed access (<a
href="https://github.com/postalsys/postal-mime/commit/fd54c37093cc64737c6bb17986bc9d052d2d5add">fd54c37</a>)</li>
</ul>
<h2>v2.7.0</h2>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.6.1...v2.7.0">2.7.0</a>
(2025-12-22)</h2>
<h3>Features</h3>
<ul>
<li>add headerLines property exposing raw header lines (<a
href="https://github.com/postalsys/postal-mime/commit/c79a02ab05d9cac44e05e95a433752ff292aa5eb">c79a02a</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/postalsys/postal-mime/blob/master/CHANGELOG.md">postal-mime's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.3...v2.7.4">2.7.4</a>
(2026-03-17)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>add missing originalKey to Header type and Uint8Array to Attachment
content (<a
href="https://github.com/postalsys/postal-mime/commit/92cc91c1c8477e0462cb0e93ddf8ea6aec6534d0">92cc91c</a>)</li>
<li>include originalKey in parsed headers output (<a
href="https://github.com/postalsys/postal-mime/commit/83521c87f62e5e095ae09913c70798f20e2ab347">83521c8</a>)</li>
<li>preserve __esModule and .default in CJS build for bundler interop
(<a
href="https://github.com/postalsys/postal-mime/commit/1466910e31608b9e5307724ecc6a0a3a70556048">1466910</a>)</li>
<li>prevent RFC 2047 encoded-word address fabrication (<a
href="https://github.com/postalsys/postal-mime/commit/844f92023d49d819ef13b9ad5c50b7c346eb02d3">844f920</a>)</li>
</ul>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.2...v2.7.3">2.7.3</a>
(2026-01-09)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>correct TypeScript type definitions to match implementation (<a
href="https://github.com/postalsys/postal-mime/commit/b225d7cca422cb9bc3ab5301e94c4c0bef9a69e2">b225d7c</a>)</li>
</ul>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.1...v2.7.2">2.7.2</a>
(2026-01-08)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>add null checks for contentType.parsed access (<a
href="https://github.com/postalsys/postal-mime/commit/ad8f4c62e0972fd0244859ee5a5184b2cac26395">ad8f4c6</a>)</li>
<li>improve RFC compliance for MIME parsing (<a
href="https://github.com/postalsys/postal-mime/commit/e004c3acb29d72ed7eaf1b0b66351cf8b82b970d">e004c3a</a>)</li>
</ul>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.0...v2.7.1">2.7.1</a>
(2025-12-22)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>Add null checks for contentDisposition.parsed access (<a
href="https://github.com/postalsys/postal-mime/commit/fd54c37093cc64737c6bb17986bc9d052d2d5add">fd54c37</a>)</li>
</ul>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.6.1...v2.7.0">2.7.0</a>
(2025-12-22)</h2>
<h3>Features</h3>
<ul>
<li>add headerLines property exposing raw header lines (<a
href="https://github.com/postalsys/postal-mime/commit/c79a02ab05d9cac44e05e95a433752ff292aa5eb">c79a02a</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/postalsys/postal-mime/commit/178f1ef0b1cd0047e1b8e690beabfec541b4daa7"><code>178f1ef</code></a>
chore(master): release 2.7.4 (<a
href="https://redirect.github.com/postalsys/postal-mime/issues/88">#88</a>)</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/1f7ba618d42d34b779157dfa33794cbae383a24d"><code>1f7ba61</code></a>
chore: bump devDependencies</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/83521c87f62e5e095ae09913c70798f20e2ab347"><code>83521c8</code></a>
fix: include originalKey in parsed headers output</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/b0d7b11550a2a3c65a52a2adf4f8281058023cab"><code>b0d7b11</code></a>
test: improve test coverage across codebase</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/ebc5ce619649d13ad72f4d12414f3e337a9e248c"><code>ebc5ce6</code></a>
refactor: simplify and clean up codebase</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/1466910e31608b9e5307724ecc6a0a3a70556048"><code>1466910</code></a>
fix: preserve __esModule and .default in CJS build for bundler
interop</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/844f92023d49d819ef13b9ad5c50b7c346eb02d3"><code>844f920</code></a>
fix: prevent RFC 2047 encoded-word address fabrication</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/24dc6c64dfb43d89a8c8837ec941c96ebfa2c1fa"><code>24dc6c6</code></a>
test: update type check test with originalKey property</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/92cc91c1c8477e0462cb0e93ddf8ea6aec6534d0"><code>92cc91c</code></a>
fix: add missing originalKey to Header type and Uint8Array to Attachment
content</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/aa5baeafa6ffd093ab447c22d20e5da25051faff"><code>aa5baea</code></a>
docs: add link to full documentation site</li>
<li>Additional commits viewable in <a
href="https://github.com/postalsys/postal-mime/compare/v2.6.1...v2.7.4">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=postal-mime&package-manager=npm_and_yarn&previous-version=2.6.1&new-version=2.7.4)](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 <charles@twenty.com>
2026-04-30 11:38:44 +00:00
3fbb70c13f i18n - docs translations (#20157)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-30 12:49:31 +02:00
c6b6c824d4 i18n - translations (#20156)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-30 11:55:45 +02:00
b21bf66b38 i18n - translations (#20155)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-30 11:50:00 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
22838ac6de chore(deps-dev): bump @babel/preset-typescript from 7.24.7 to 7.28.5 (#20151)
Bumps
[@babel/preset-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript)
from 7.24.7 to 7.28.5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/babel/babel/releases"><code>@​babel/preset-typescript</code>'s
releases</a>.</em></p>
<blockquote>
<h2>v7.28.5 (2025-10-23)</h2>
<p>Thank you <a
href="https://github.com/CO0Ki3"><code>@​CO0Ki3</code></a>, <a
href="https://github.com/Olexandr88"><code>@​Olexandr88</code></a>, and
<a href="https://github.com/youthfulhps"><code>@​youthfulhps</code></a>
for your first PRs!</p>
<h4>👓 Spec Compliance</h4>
<ul>
<li><code>babel-parser</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17446">#17446</a>
Allow <code>Runtime Errors for Function Call Assignment Targets</code>
(<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
<li><code>babel-helper-validator-identifier</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17501">#17501</a>
fix: update identifier to unicode 17 (<a
href="https://github.com/fisker"><code>@​fisker</code></a>)</li>
</ul>
</li>
</ul>
<h4>🐛 Bug Fix</h4>
<ul>
<li><code>babel-plugin-proposal-destructuring-private</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17534">#17534</a>
Allow mixing private destructuring and rest (<a
href="https://github.com/CO0Ki3"><code>@​CO0Ki3</code></a>)</li>
</ul>
</li>
<li><code>babel-parser</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17521">#17521</a>
Improve <code>@babel/parser</code> error typing (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
<li><a
href="https://redirect.github.com/babel/babel/pull/17491">#17491</a>
fix: improve ts-only declaration parsing (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
<li><code>babel-plugin-proposal-discard-binding</code>,
<code>babel-plugin-transform-destructuring</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17519">#17519</a>
fix: <code>rest</code> correctly returns plain array (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
<li><code>babel-helper-create-class-features-plugin</code>,
<code>babel-helper-member-expression-to-functions</code>,
<code>babel-plugin-transform-block-scoping</code>,
<code>babel-plugin-transform-optional-chaining</code>,
<code>babel-traverse</code>, <code>babel-types</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17503">#17503</a> Fix
<code>JSXIdentifier</code> handling in
<code>isReferencedIdentifier</code> (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
<li><code>babel-traverse</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17504">#17504</a>
fix: ensure scope.push register in anonymous fn (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
</ul>
<h4>🏠 Internal</h4>
<ul>
<li><code>babel-types</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17494">#17494</a>
Type checking babel-types scripts (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
</ul>
<h4>🏃‍♀️ Performance</h4>
<ul>
<li><code>babel-core</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17490">#17490</a>
Faster finding of locations in <code>buildCodeFrameError</code> (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
</ul>
<h4>Committers: 8</h4>
<ul>
<li>Babel Bot (<a
href="https://github.com/babel-bot"><code>@​babel-bot</code></a>)</li>
<li>Byeongho Yoo (<a
href="https://github.com/youthfulhps"><code>@​youthfulhps</code></a>)</li>
<li>Huáng Jùnliàng (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
<li>Hyeon Dokko (<a
href="https://github.com/CO0Ki3"><code>@​CO0Ki3</code></a>)</li>
<li>Nicolò Ribaudo (<a
href="https://github.com/nicolo-ribaudo"><code>@​nicolo-ribaudo</code></a>)</li>
<li><a
href="https://github.com/Olexandr88"><code>@​Olexandr88</code></a></li>
<li><a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a></li>
<li>fisker Cheung (<a
href="https://github.com/fisker"><code>@​fisker</code></a>)</li>
</ul>
<h2>v7.28.4 (2025-09-05)</h2>
<p>Thanks <a
href="https://github.com/gwillen"><code>@​gwillen</code></a> and <a
href="https://github.com/mrginglymus"><code>@​mrginglymus</code></a> for
your first PRs!</p>
<h4>🏠 Internal</h4>
<ul>
<li><code>babel-core</code>,
<code>babel-helper-check-duplicate-nodes</code>,
<code>babel-traverse</code>, <code>babel-types</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17493">#17493</a>
Update Jest to v30.1.1 (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
<li><code>babel-plugin-transform-regenerator</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17455">#17455</a>
chore: Clean up <code>transform-regenerator</code> (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/babel/babel/blob/main/CHANGELOG.md"><code>@​babel/preset-typescript</code>'s
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<blockquote>
<p><strong>Tags:</strong></p>
<ul>
<li>💥 [Breaking Change]</li>
<li>👓 [Spec Compliance]</li>
<li>🚀 [New Feature]</li>
<li>🐛 [Bug Fix]</li>
<li>📝 [Documentation]</li>
<li>🏠 [Internal]</li>
<li>💅 [Polish]</li>
</ul>
</blockquote>
<p><em>Note: Gaps between patch versions are faulty, broken or test
releases.</em></p>
<p>This file contains the changelog starting from v8.0.0-alpha.0.</p>
<ul>
<li>See <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v7.15.0-v7.28.5.md">CHANGELOG
- v7.15.0 to v7.28.5</a> for v7.15.0 to v7.28.5 changes (the last common
release between the v8 and v7 release lines was v7.28.5).</li>
<li>See <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v7.0.0-v7.14.9.md">CHANGELOG
- v7.0.0 to v7.14.9</a> for v7.0.0 to v7.14.9 changes.</li>
<li>See <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v7-prereleases.md">CHANGELOG
- v7 prereleases</a> for v7.0.0-alpha.1 to v7.0.0-rc.4 changes.</li>
<li>See <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v4.md">CHANGELOG
- v4</a>, <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v5.md">CHANGELOG
- v5</a>, and <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v6.md">CHANGELOG
- v6</a> for v4.x-v6.x changes.</li>
<li>See <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-6to5.md">CHANGELOG
- 6to5</a> for the pre-4.0.0 version changelog.</li>
<li>See <a
href="https://github.com/babel/babel/blob/main/packages/babel-parser/CHANGELOG.md">Babylon's
CHANGELOG</a> for the Babylon pre-7.0.0-beta.29 version changelog.</li>
<li>See <a
href="https://github.com/babel/babel-eslint/releases"><code>babel-eslint</code>'s
releases</a> for the changelog before <code>@babel/eslint-parser</code>
7.8.0.</li>
<li>See <a
href="https://github.com/babel/eslint-plugin-babel/releases"><code>eslint-plugin-babel</code>'s
releases</a> for the changelog before <code>@babel/eslint-plugin</code>
7.8.0.</li>
</ul>
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<h2>v8.0.0-rc.4 (2026-04-29)</h2>
<h4>👓 Spec Compliance</h4>
<ul>
<li><code>babel-parser</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17954">#17954</a>
fix(parser): ts parser small fixes (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
<li><a
href="https://redirect.github.com/babel/babel/pull/17923">#17923</a>
Support flow extends bound (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
<li><a
href="https://redirect.github.com/babel/babel/pull/17888">#17888</a> TS
parser small fixes (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
<li><a
href="https://redirect.github.com/babel/babel/pull/17865">#17865</a>
Fix(parser): flow parser small fixes (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
<li><code>babel-generator</code>, <code>babel-parser</code>,
<code>babel-plugin-transform-spread</code>, <code>babel-types</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17871">#17871</a>
Disallow super call after new (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
</ul>
<h4>💥 Breaking Change</h4>
<ul>
<li><code>babel-cli</code>,
<code>babel-helper-transform-fixture-test-runner</code>,
<code>babel-helpers</code>, <code>babel-node</code>,
<code>babel-register</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17938">#17938</a>
Bundle more packages (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
<li><code>babel-traverse</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17937">#17937</a>
Remove <code>Scope#buildUndefinedNode</code> (<a
href="https://github.com/nicolo-ribaudo"><code>@​nicolo-ribaudo</code></a>)</li>
</ul>
</li>
<li><code>babel-helper-wrap-function</code>,
<code>babel-plugin-transform-block-scoping</code>,
<code>babel-plugin-transform-regenerator</code>,
<code>babel-traverse</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17907">#17907</a>
Remove <code>NodePath#toComputedKey</code> (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
<li><code>babel-plugin-external-helpers</code>,
<code>babel-template</code>, <code>babel-traverse</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17830">#17830</a>
Replace remaining whitelist/blacklist with inclusive alternatives (<a
href="https://github.com/stuckvgn"><code>@​stuckvgn</code></a>)</li>
</ul>
</li>
<li><code>babel-plugin-transform-property-mutators</code>,
<code>babel-standalone</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17882">#17882</a>
Remove <code>@babel/plugin-transform-property-mutators</code> (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/babel/babel/commit/61647ae2397c82c3c71f077b5ab109106a5cac0f"><code>61647ae</code></a>
v7.28.5</li>
<li><a
href="https://github.com/babel/babel/commit/42cb285b59fc99a8102d69bef6223b75617e9f46"><code>42cb285</code></a>
Improve <code>@babel/core</code> types (<a
href="https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript/issues/17404">#17404</a>)</li>
<li><a
href="https://github.com/babel/babel/commit/eebd3a06021c13d335b5b0bd79734df3abbea678"><code>eebd3a0</code></a>
v7.27.1</li>
<li><a
href="https://github.com/babel/babel/commit/fdc0fb59e119ee0b38bced63867a344a5b4bc2f3"><code>fdc0fb5</code></a>
[Babel 8] Bump nodejs requirements to <code>^20.19.0 || &gt;=
22.12.0</code> (<a
href="https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript/issues/17204">#17204</a>)</li>
<li><a
href="https://github.com/babel/babel/commit/5c350eab83dd12268add44cce0eeda6c898211e3"><code>5c350ea</code></a>
v7.27.0</li>
<li><a
href="https://github.com/babel/babel/commit/ca4865a7f43a6a56aec242e23e4a3e318cf0ca92"><code>ca4865a</code></a>
Fix: align behaviour to tsc <code>rewriteRelativeImportExtensions</code>
(<a
href="https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript/issues/17118">#17118</a>)</li>
<li><a
href="https://github.com/babel/babel/commit/cd24cc07ef6558b7f6510f9177f6393c91b0549f"><code>cd24cc0</code></a>
chore: Update TS 5.7 (<a
href="https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript/issues/17053">#17053</a>)</li>
<li><a
href="https://github.com/babel/babel/commit/63d30381c169780460e01bdb6669c5e01af1dfbe"><code>63d3038</code></a>
v7.26.0</li>
<li><a
href="https://github.com/babel/babel/commit/bfa56c49569f0bfd5579e0e1870ffa92f74fad48"><code>bfa56c4</code></a>
Support <code>import()</code> in <code>rewriteImportExtensions</code>
(<a
href="https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript/issues/16794">#16794</a>)</li>
<li><a
href="https://github.com/babel/babel/commit/b07957ebb316a1e2fc67454fc7423508bb942e63"><code>b07957e</code></a>
v7.25.9</li>
<li>Additional commits viewable in <a
href="https://github.com/babel/babel/commits/v7.28.5/packages/babel-preset-typescript">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~GitHub%20Actions">GitHub Actions</a>, a new
releaser for <code>@​babel/preset-typescript</code> since your current
version.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@babel/preset-typescript&package-manager=npm_and_yarn&previous-version=7.24.7&new-version=7.28.5)](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-04-30 09:32:13 +00:00
martmullandGitHub bddd23fd9c Fix application icons (#20142)
fixes application chip (icon Name) in all setting tables

## After
<img width="1200" height="896" alt="image"
src="https://github.com/user-attachments/assets/bd377f47-1d52-4142-b904-f2ce90c1db78"
/>
<img width="1200" height="917" alt="image"
src="https://github.com/user-attachments/assets/f49cc742-f11e-47e3-86ed-34beffe493c7"
/>
<img width="1234" height="878" alt="image"
src="https://github.com/user-attachments/assets/2ab459de-5f9d-4d39-9490-eec4ed9ee432"
/>
<img width="1239" height="845" alt="image"
src="https://github.com/user-attachments/assets/3c1bf258-285a-47b9-a60d-05ba1564334d"
/>
<img width="1183" height="907" alt="image"
src="https://github.com/user-attachments/assets/715b2470-2d88-48e3-88ac-d3daf3451717"
/>
<img width="1300" height="912" alt="image"
src="https://github.com/user-attachments/assets/d7c829fa-bf1d-4f19-82de-a8bf29e22bfa"
/>
2026-04-30 09:32:04 +00:00
842e679cc6 fix(billing): gate AI credit-cap at entry points instead of workflow executor (#20096)
## Background

The 2026-04-26 incident saw 716M Sonnet 4.6 tokens consumed in a single
trial workspace. Two causes: failed agent executions weren't billed
(addressed by #20065) and the credit-cap gate had been removed from
`WorkflowExecutorWorkspaceService.executeStep` in #19904, leaving no
enforcement point at all.

## Why not just revert #19904

#19904 was right that gating at the workflow executor is too coarse.
When one user exhausted a workspace's credits via chat, *all* workflows
hard-failed mid-run — including cheap DB/CRUD/branch automations costing
essentially nothing. Reverting would re-introduce that cliff.

## New design: gate at the AI entry points

The chat resolver already gates this way
(`agent-chat.resolver.ts:137-148`). This PR replicates the same pattern
at every other point where the workspace can incur real AI cost:

- `executeAgent` in `agent-async-executor.service.ts`
- the REST handler in `ai-generate-text.controller.ts`
- `generateThreadTitle` in `agent-title-generation.service.ts`

In each, after auth/validation: skip if `IS_BILLING_ENABLED` is false;
otherwise call `BillingService.canBillMeteredProduct(workspaceId,
BillingProductKey.WORKFLOW_NODE_EXECUTION)`; on `false`, throw
`BillingException(BILLING_CREDITS_EXHAUSTED)`. No new method, no new
exception code, no new product key.

This matches industry convention (Lovable/Replit also gate at the
expensive-operation boundary, not at every cheap step).

## Deliberately not gated

- `WorkflowExecutorWorkspaceService.executeStep` — the design choice is
now intentional, so the #19904 TODO is replaced by a one-line
absolute-behavior comment explaining why the gate isn't here. Cheap
workflow steps (DB CRUD, branching, action steps) are not gated, so a
chat-driven cap exhaustion does not block non-AI automations.
- `repair-tool-call.util` — repair is a sub-call inside an already-gated
AI flow. If the parent is gated, repair will naturally not run. Adding a
gate here adds complexity without value.

## Net effect

A workspace that exhausts credits via chat or AI agent stops making AI
calls. Its non-AI workflows continue running normally. A workflow with
both AI and non-AI steps fails at the AI step with
`BILLING_CREDITS_EXHAUSTED`, but downstream non-AI steps that don't
depend on the AI output still run.

## Conflicts

This PR overlaps with three other in-flight PRs in the same files. None
of them touch the gate logic; rebasing on top of any of them is trivial:

- #20065 (agent-async-executor): adds `workspaceId` to `executeAgent`
args and bills in `finally`. The gate at the top of `executeAgent` from
this PR sits naturally above that.
- #20066 (REST controller): adds usage billing to the controller.
- #20067 (title gen): adds usage billing to title generation and
tool-call repair.

Recommend landing #20065/#20066/#20067 first; this PR rebases trivially
on top.

## Tests

Out of scope per the PR series convention. The existing chat-resolver
gate isn't unit-tested either; this PR follows the same precedent.
Follow-up: add integration coverage that exercises a workspace at
`hasReachedCurrentPeriodCap=true` against each of the three new gates
plus the pre-existing chat-resolver gate.

## Future follow-ups

- Per-user soft cap inside a workspace (the Lovable Business-tier
pattern), so one user can't exhaust the workspace's cap.
- Pre-flight cost estimate so the user sees an "approaching cap" warning
before the hard stop.
- Rename `BillingProductKey.WORKFLOW_NODE_EXECUTION` — the name predates
this design choice and is misleading now that it gates AI entry points
rather than workflow nodes.

## Test plan

- [ ] Trigger a workspace into `hasReachedCurrentPeriodCap=true`.
- [ ] Send a chat message — expect failure with
`BILLING_CREDITS_EXHAUSTED`.
- [ ] Run a workflow whose only AI step is an `ai-agent` action — expect
that step to fail with `BILLING_CREDITS_EXHAUSTED`, downstream non-AI
steps still run.
- [ ] POST to `/rest/ai/generate-text` — expect
`BILLING_CREDITS_EXHAUSTED`.
- [ ] Create a new chat thread (which kicks off `generateThreadTitle`) —
expect `BILLING_CREDITS_EXHAUSTED`.
- [ ] Run a workflow with no AI step (only DB CRUD/branching/actions) —
expect it to run unaffected.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 18:48:06 +02:00
neo773andGitHub a713f8d87f CalDAV: support Digest auth (#20135)
Adds digest auth support for CalDAV, mostly used by legacy servers

/closes https://github.com/twentyhq/twenty/issues/19922
2026-04-29 16:09:21 +00:00
EtienneandGitHub fd6d5f895d Ai Chat - Caching optim (#20126)
EDIT : 
- solving auto-caching from Anthropic by updating ai-sdk/anthropic +
adding providerOption at stream level
- concerning Bedrock, it needs breakpoint


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

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

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

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

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

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

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

**3. Persisted cached token usage to monitor cache strat. efficiency**
2026-04-29 14:42:12 +00:00
11628d19a3 add recurring calendar events for google cal (#19748)
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-29 14:06:57 +00:00
46ba5fd16c i18n - translations (#20138)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-29 15:51:28 +02:00
nitinandGitHub 4649736d49 [Command Menu] Fix record-selection command filtering in edit mode (#20034)
https://github.com/user-attachments/assets/fe1461c7-0d5c-4c6f-8c2e-2cf569e7de90

## What

Fix `RECORD_SELECTION` items leaking into the command menu when nothing
is selected, and unify how the menu renders in normal vs edit mode.

## The bug

`RECORD_SELECTION`-availability items were showing up even when
`numberOfSelectedRecords === 0`. New util
`doesCommandMenuItemMatchSelectionState` gates them, applied
consistently in the runtime provider and the editor.

## The refactor

`PinnedCommandMenuItemButtonsEditMode` was a 140-line near-duplicate of
`PinnedCommandMenuItemButtons` with its own (drifting) filter logic.
Killed it. Edit mode now flows through the same
`CommandMenuContextProvider` with a new `isInPreviewMode` flag — one
filter chain, one rendering path.

## Behavior in edit mode

**Header (pinned buttons in page header):**
- Runs the full filter chain — object metadata, page type, selection
state, page layout, *and the conditional availability expression*
- Buttons render at full styling but are inert via `pointer-events:
none` + `cursor: not-allowed`
- Preview now reflects exactly what users will see on the live page (not
a grayed-out approximation)

**Side panel editor:**
- New `useEditableCommandMenuItems` hook
- Same filters as runtime *minus* the conditional availability
expression and `FALLBACK` items — so it surfaces everything that's
actually configurable for this page context
- Still gates on selection state — if no records selected,
`RECORD_SELECTION` items are hidden from the editor too. Open to
feedback if we'd rather always show them so users can pin them ahead of
time.

## Misc

- `usePinnedCommandMenuItemsInlineLayout` — visible count now waits
until every item is measured before committing. Fixes a flash of wrong
counts on mount/resize
- Renamed `useCommandMenuContextApi` → `useCurrentCommandMenuContextApi`
— name now conveys it reads from the *current* scoped context store
- Copy: "Records selected" → "Record(s) selected"
2026-04-29 13:34:28 +00:00
Abdullah.andGitHub 51384bc085 refactor: optimize website visual runtime (#20120)
Refactors the website visual runtime to make WebGL-heavy sections more
reliable and less expensive.

This adds shared image/model loading caches, safer WebGL context
recovery, staggered visual mounting, and static rendering for decorative
Helped card visuals. It also removes a large bespoke Helped renderer in
favor of the shared halftone model canvas, reduces scroll/layout work in
the Helped section, and cleans up duplicated model-loading code across
several visuals.
2026-04-29 12:44:47 +00:00
neo773andGitHub abfa6200dd ssrf hardening (#19963)
Hardened CalDav with new approach of wrapping axios ssrf http agent to
fetch via `@lifeomic/axios-fetch` because `tsdav` only accept `fetch`
override.

Also Hardened test endpoint
2026-04-29 12:13:19 +00:00
nitinGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>nitin
480e5796ec fix(ai): render record links inside markdown headings in AI chat (#20074)
## Summary

Adds `h1`–`h6` component overrides to `LazyMarkdownRenderer` so that
`[[record:...]]` references placed inside markdown headings in the AI
chat are parsed by `processChildrenForRecordLinks` and rendered as
clickable `RecordLink` chips, matching the behavior already in place for
`p`, `li`, `td`, `th`, and `a`.

Fixes #20072

## Test plan

- [ ] In AI chat, ask a question whose answer places a record reference
inside a markdown heading (e.g. `## Found [[person:uuid:John Doe]]`) and
confirm a clickable `RecordLink` chip renders instead of the raw
`[[...]]` text.
- [ ] Verify heading styling (sizes, weights, margins) is unchanged.

Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: nitin <ehconitin@users.noreply.github.com>
2026-04-29 13:50:32 +02:00
Félix MalfaitGitHubClaude Opus 4.7claude[bot] <41898282+claude[bot]@users.noreply.github.com>
d8e2de48e6 Fix stale UI state after stop-impersonation (#20088)
## Summary

A customer reported that after **Stop Impersonating**, the sidebar still
showed the impersonated user's pinned favorites, the AI chat tab toggle,
and the AI chat history — even though the original admin's session was
correctly restored.

## Root cause

The refactor in #19597 replaced the previous `signOut()`-based stop flow
with an in-place token swap, but only cleared Apollo cache + reloaded
the user. Several user-scoped client stores were left untouched:

- **`metadataStoreState`** is localStorage-backed
(`navigationMenuItems`,
  `agentChatThreads`, `views`, `pageLayouts`, etc.) and only refreshed
  by `MinimalMetadataLoadEffect`. That effect is gated by
  `metadataLoadedVersion` + `desiredLoadState`, neither of which flips
  on a same-workspace token swap, so the effect never re-runs.
- **In-memory AI atoms** (`currentAiChatThreadState`,
`agentChatInputState`,
  `hasInitializedAgentChatThreadsState`) keep pointing at the
  impersonated user's selected thread / input.
- **Session localStorage keys** (`agentChatDraftsByThreadIdState`,
`lastVisitedObjectMetadataItemIdState`,
`lastVisitedViewPerObjectMetadataItemState`,
  `playgroundApiKeyState`) carry the impersonated user's drafts and
  navigation state.

`clearSession()` (used by logout) avoids this because it calls
`applyMockedMetadata()` and flips `desiredLoadState` mocked↔real, which
chain-triggers a full metadata reload on next sign-in.

## Fix

Extract a `resetUserScopedClientState` helper inside
`useImpersonationSession` that:

1. Calls `clearSessionLocalStorageKeys()` to drop user-scoped
localStorage
   keys.
2. Resets the in-memory AI session atoms.
3. Marks `metadataStoreState['agentChatThreads']` as `'empty'`.
`useLoadStaleMetadataEntities` does **not** handle this entity key, so
   without an explicit reset to `'empty'` the
   `AgentChatThreadInitializationEffect` (which only fires on `'empty'`)
   would never refetch.
4. Calls `invalidateMetadataStore()` to clear all
`currentCollectionHash`
   values and bump `metadataLoadedVersion`, forcing
   `MinimalMetadataLoadEffect` to re-run and refetch
   `navigationMenuItems`, `views`, `pageLayouts`, etc. against the new
   token.

The helper is applied to both `startImpersonating` and
`stopImpersonating`
— start had the same latent bug; the impersonated user could see the
admin's favorites until the cache happened to refresh.

## Test plan

- [ ] As an admin user, pin some favorites in the sidebar
- [ ] Impersonate a user with different favorites → favorites should
      switch to the impersonated user's
- [ ] Click "Stop Impersonating" → sidebar should immediately show the
      admin's favorites (not the impersonated user's)
- [ ] As an admin **without** AI permission, impersonate a user **with**
AI permission, open AI chat, send a message, then stop impersonating
      → AI chat history should be empty / inaccessible (the AI tab
      visibility itself is fixed in a separate PR)
- [ ] Type a draft in AI chat as the impersonated user → after stop, the
      draft should be gone
- [ ] Verify regular sign-out still works while impersonating
- [ ] Verify the impersonation banner still shows / hides correctly

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-04-29 13:37:12 +02:00
Paul RastoinandGitHub b49e58dfc6 Fix click house migration (#20127)
`it does not support renaming of multiple tables in single query.`

@etiennejouan manually fix the corrupted clickhouse instance
2026-04-29 10:48:21 +00:00
a6118b7dc3 i18n - translations (#20125)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-29 11:57:14 +02:00
Abdul RahmanandGitHub 19ee9444ed add UpsertViewWidget resolver (#20053) 2026-04-29 09:41:02 +00:00
Paul RastoinandGitHub b92617d46e Copy twenty-shared in twenty-website deploy (#20124) 2026-04-29 08:44:54 +00:00
c476c6c80b chore: sync AI model catalog from models.dev (#20122)
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-04-29 08:45:09 +02:00
Paul RastoinandGitHub d2dda67596 Fix upgrade --start-from-workspace-id (#20116)
# Introduction
Prevent using both `--start-from-workspace-id` and `--workspace`

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

When `--start-from-workspace-id` is passed we filter from all the
fetched active or suspended workspace ids and apply equivalent filter as
before
2026-04-28 16:46:15 +00:00
Abdullah.andGitHub 6aec449a56 refactor: harden website runtime, routing, and hero visual (#20113)
This centralizes routing/SEO ownership, replaces deprecated middleware
with proxy, adds safer lifecycle/runtime primitives, and introduces
visual error boundaries so broken WebGL/canvas-heavy visuals can fail
gracefully instead of taking down the page. It also hardens animation,
resize, visibility, cleanup, and WebGL fallback paths for a broader
range of browsers and devices.

The hero visual was split from large monolithic files into focused
domain folders for shell, pages, shared primitives, window interactions,
terminal conversation, prompt, editor, and traffic-light behavior.
Legacy unused section code was removed, visual configs were extracted,
state/geometry logic was moved into testable modules, and coverage was
added across routing, SEO, lifecycle, animation, visual runtime,
halftone behavior, and hero interactions.

More changes on the way, but this should make the website a lot more
stable - disabling WebGL on Firefox and loading the website does not
cause crashes on local any longer, will test on dev once this is merged.
2026-04-28 17:15:28 +02:00
7ea1dfdd49 i18n - translations (#20115)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-28 16:48:25 +02:00
3290bf3ab1 fix(rest-api): prevent silent pagination failures and include valid options in enum validation errors (#20092)
# Summary 
(fixes #20044)

This PR implements two fixes for the REST API to enforce stricter
validation and provide better error messages.

Issue 1: Cursor parameter silently ignored
Problem: When users provided common cursor aliases (e.g., cursor, after,
before) instead of the correct parameter names (starting_after,
ending_before), the API silently ignored them and returned page 1 on
every request.
Solution: Added strict validation to detect common cursor aliases and
throw a clear error directing users to use the correct parameter names.
Files modified:
-
packages/twenty-server/src/engine/api/rest/input-request-parsers/rest-input-request-parser.exception.ts
-
packages/twenty-server/src/engine/api/rest/input-request-parsers/starting-after-parser-utils/parse-starting-after-rest-request.util.ts
-
packages/twenty-server/src/engine/api/rest/input-request-parsers/ending-before-parser-utils/parse-ending-before-rest-request.util.ts
- Test files for both parsers
Example error:
Invalid cursor parameter 'cursor'. Use 'starting_after' for pagination.
---
Issue 2: OpportunityStageEnum not validated on REST
Problem: When creating or updating opportunities via REST with an
invalid stage value, the API either silently dropped the value or
returned a generic error without listing valid options.
Solution: Updated the SELECT field validation to include valid options
in the error message.
Files modified:
-
packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-rating-and-select-field-or-throw.util.ts
- Test file
Example error:
Invalid value "BAD_VALUE" for field "stage". Valid values are: NEW,
SCREENING, MEETING, PROPOSAL, CUSTOMER
---
### Testing
- Added 5 new test cases for `parse-starting-after-rest-request.util.ts`
- Added 6 new test cases for `parse-ending-before-rest-request.util.ts`
- Added 1 new test case for
`validate-rating-and-select-field-or-throw.util.ts`
- All 21 tests passing
---
Breaking Changes
None - correct usage is unaffected.

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-04-28 14:31:18 +00:00
EtienneandGitHub fbaea0639a Billing - optimize usageEvent CH table (#20019)
- Update usageEvent clickhouse table, partitioning, indexing and
projection (auto materialized view) to optimize credit usage queries
- Add caching for available credits and billing subscription


To do in next PR: deprecate enforceCapUsage cron. Bonus : real-time on
billingSubscription
2026-04-28 14:06:49 +00:00
2a3b8adcfb i18n - translations (#20114)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-28 16:17:27 +02:00
Félix MalfaitGitHubClaude Opus 4.7claude[bot] <claude[bot]@users.noreply.github.com>
8f362186ce Redesign application content tab + logic function settings; add Layout detail pages (#20056)
## Summary

Iterative redesign of two related areas in settings, plus a new
`pages/settings/layout/` folder for read-only entity detail pages.

### Application content tab

- **Grouped into three sections** — Data / Layout / Logic — each with
one H2 + multiple `TableSection`-wrapped sub-tables (mirrors the
role-permissions pattern). Replaces six per-category table/row
components with one uniform `<SettingsApplicationContentSubtable>` +
`ApplicationContentRow` shape (net **−~700 lines** across the refactor).
- **All 10 row categories now clickable** for installed apps:
- Objects / Fields / Logic functions / Front components → existing
detail pages
  - Agents → existing `AiAgentDetail`
- Skills → existing `AiSkillDetail` (looked up by `Skill.applicationId +
name`)
- Roles → existing `RoleDetail` (looked up by
`Role.universalIdentifier`)
- Views / Page layouts / Navigation menu items → **new** detail pages
(see below)
- **Lifecycle hooks visible** — `pre-install` / `post-install` logic
functions are surfaced in the Trigger column instead of appearing as
empty/misconfigured.

### Logic function settings (Triggers + Test tabs)

- Triggers tab is now editable (HTTP / Cron / Database event / AI tool)
with a `<SettingsLogicFunctionTriggerSection>` wrapper that owns the
toggle, header, and read-only short-circuit.
- HTTP section gets a Live URL field with copy-to-clipboard.
- Each section shows a **Sample input** preview (the JSON the function
will receive) using the same payload builders the Test tab uses.
- Test tab: **Simulate trigger** buttons that prefill the JSON input
from the configured trigger's schema. Replaces an unclickable `<Select>`
(which auto-disables when there's only one option — the typical case).
- Read-only behavior for installed-app functions: explicit `<Callout>`
notice when there's no trigger; trigger sections render as disabled
controls when there is one.
- Removed the empty Environment Variables section from the Settings tab
(it just told the user to go elsewhere).

### New `pages/settings/layout/` folder

Three new app-scoped detail pages so users can drill into entities the
GraphQL `Application` type doesn't expose by id (keyed by manifest
`universalIdentifier`):

- `ApplicationViewDetail` — type, object, visibility + Fields / Filters
/ Sorts subsections (field UIDs resolved to readable labels via
`useFieldLabelByUid`)
- `ApplicationPageLayoutDetail` — type, object + per-tab subsections
listing widgets
- `ApplicationNavigationMenuItemDetail` — type, destination (resolved),
icon, color, position

Each page reads from the marketplace manifest the parent app page
already loads (no extra queries). Folder set up so a future "Layout"
settings tab can grow here (analogous to the existing `data-model/`
folder under the Data tab).

### Other consistency fixes

- Breadcrumbs on every app-scoped entity detail page now include a
category crumb so users know what they're looking at: `Workspace /
Applications / Timely / Navigation menu items / Time entry`.
- Title fallback for nav menu items uses the resolved destination
(`"Time entry"`) instead of the raw enum (`"OBJECT"`).
- New shared utils: `getNavigationMenuItemDestination`,
`resolveManifestObjectLabel`, `getLogicFunctionTriggerLabel`,
`<MonoText>`.

## Backend changes

Only one minor schema-shape change (additive): added `applicationId` to
the `SkillFields` GraphQL fragment and `universalIdentifier` to the
`RoleFragment` so the new lookups have what they need. Generated
metadata schema patched in-tree to match — regenerate with `nx run
twenty-front:graphql:generate --configuration=metadata` if it drifts.

## Test plan

- [ ] Application content tab on an installed app shows the 3 grouped
sections; rows in each section are clickable
- [ ] Click an Object → existing object detail page
- [ ] Click a Field → existing field-edit page
- [ ] Click an Agent / Skill / Role → existing detail page
- [ ] Click a View / Page layout / Navigation menu item → new read-only
detail page; subsections (Fields/Filters/Sorts for views, per-tab
widgets for page layouts) populate correctly
- [ ] Breadcrumbs on every entity detail page have 5 crumbs ending in
`<Category> / <Entity name>`
- [ ] Logic function Triggers tab: toggle each trigger type on/off, see
the Sample input preview update; for installed apps, sections render as
read-only
- [ ] Test tab: each "Simulate trigger" button prefills the JSON editor
with the matching payload shape
- [ ] Functions list: a function configured as `post-install` shows
"Post-install" in the Trigger column

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: claude[bot] <claude[bot]@users.noreply.github.com>
2026-04-28 16:08:15 +02:00
martmullandGitHub 8998009805 Stop reseting isListed and is featured after each sync (#20111)
isListed and isFeatured are manually updated by the admin, it should not
be updated if the application already exists
2026-04-28 13:23:42 +00:00
Abdul RahmanandGitHub a710c105cf Fix orphan views by deferring record table widget view creation to dashboard save (#20006) 2026-04-28 10:06:26 +00:00
1335 changed files with 78221 additions and 38901 deletions
+11
View File
@@ -7,6 +7,17 @@ inputs:
runs:
using: 'composite'
steps:
- name: Free disk space for install
if: runner.os == 'Linux'
shell: bash
run: |
# Default GitHub images ship large SDKs this repo does not use; removing
# them avoids ENOSPC when restoring or linking a full Yarn node_modules.
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/local/lib/android
sudo rm -rf /opt/ghc
sudo rm -rf /opt/hostedtoolcache/CodeQL
df -h
- name: Cache primary key builder
id: globals
shell: bash
+12 -13
View File
@@ -4,20 +4,19 @@
# See https://crowdin.github.io/crowdin-cli/configuration for more information
#
"preserve_hierarchy": true
"base_path": ".."
files: [
{
#
# Source files filter - PO files for Lingui
#
"source": "**/en.po",
preserve_hierarchy: true
base_path: ..
files:
#
# Source files filter - PO files for Lingui
#
- source: packages/twenty-front/src/locales/en.po
#
# Translation files path
#
"translation": "%original_path%/%locale%.po",
}
]
translation: '%original_path%/%locale%.po'
- source: packages/twenty-server/src/engine/core-modules/i18n/locales/en.po
translation: '%original_path%/%locale%.po'
- source: packages/twenty-emails/src/locales/en.po
translation: '%original_path%/%locale%.po'
+23
View File
@@ -0,0 +1,23 @@
#
# Crowdin CLI configuration for Website translations (twenty-website-new)
# Project ID: 4
# See https://crowdin.github.io/crowdin-cli/configuration for more information
#
project_id: 4
preserve_hierarchy: true
base_url: 'https://twenty.api.crowdin.com'
base_path: ..
languages_mapping:
locale:
fr: fr-FR
files:
#
# Source file - PO file for Lingui
#
- source: packages/twenty-website-new/src/locales/en.po
#
# Translation files path
#
translation: '%original_path%/%locale%.po'
-4
View File
@@ -58,7 +58,6 @@ jobs:
npx nx run twenty-server:lingui:compile --strict
npx nx run twenty-emails:lingui:compile --strict
npx nx run twenty-front:lingui:compile --strict
npx nx run twenty-website-new:lingui:compile --strict
continue-on-error: true
- name: Stash any changes before pulling translations
@@ -75,8 +74,6 @@ jobs:
upload_sources: false
upload_translations: false
download_translations: true
source: '**/en.po'
translation: '%original_path%/%locale%.po'
export_only_approved: false
localization_branch_name: i18n
base_url: 'https://twenty.api.crowdin.com'
@@ -116,7 +113,6 @@ jobs:
npx nx run twenty-server:lingui:compile
npx nx run twenty-emails:lingui:compile
npx nx run twenty-front:lingui:compile
npx nx run twenty-website-new:lingui:compile
git status
git add .
if ! git diff --staged --quiet --exit-code; then
-2
View File
@@ -41,7 +41,6 @@ jobs:
npx nx run twenty-server:lingui:extract
npx nx run twenty-emails:lingui:extract
npx nx run twenty-front:lingui:extract
npx nx run twenty-website-new:lingui:extract
- name: Check and commit extracted files
id: check_extract_changes
@@ -61,7 +60,6 @@ jobs:
npx nx run twenty-server:lingui:compile
npx nx run twenty-emails:lingui:compile
npx nx run twenty-front:lingui:compile
npx nx run twenty-website-new:lingui:compile
- name: Check and commit compiled files
id: check_compile_changes
+135
View File
@@ -0,0 +1,135 @@
# Pull down website translations from Crowdin every two hours or when triggered manually.
# When force_pull input is true, translations will be pulled regardless of compilation status.
name: 'Pull website translations from Crowdin'
permissions:
contents: write
pull-requests: write
on:
schedule:
- cron: '0 */2 * * *' # Every two hours.
workflow_dispatch:
inputs:
force_pull:
description: 'Force pull translations regardless of compilation status'
required: false
type: boolean
default: false
workflow_call:
inputs:
force_pull:
description: 'Force pull translations regardless of compilation status'
required: false
type: boolean
default: false
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
pull_website_translations:
name: Pull website translations
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ github.token }}
ref: ${{ github.head_ref || github.ref_name }}
- name: Setup website i18n branch
run: |
git fetch origin i18n-website || true
git checkout -B i18n-website origin/i18n-website || git checkout -b i18n-website
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build twenty-shared
run: npx nx build twenty-shared
# Strict mode fails if there are missing website translations.
- name: Compile website translations
id: compile_translations_strict
run: npx nx run twenty-website-new:lingui:compile --strict
continue-on-error: true
- name: Stash any changes before pulling translations
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@twenty.com'
git add .
git stash
- name: Pull website translations from Crowdin
if: inputs.force_pull || steps.compile_translations_strict.outcome == 'failure'
uses: crowdin/github-action@v2
with:
upload_sources: false
upload_translations: false
download_translations: true
source: 'packages/twenty-website-new/src/locales/en.po'
translation: 'packages/twenty-website-new/src/locales/%locale%.po'
export_only_approved: false
localization_branch_name: i18n-website
base_url: 'https://twenty.api.crowdin.com'
auto_approve_imported: false
import_eq_suggestions: false
download_sources: false
push_sources: false
skip_untranslated_strings: false
skip_untranslated_files: false
push_translations: false
create_pull_request: false
skip_ref_checkout: true
dryrun_action: false
config: '.github/crowdin-website.yml'
env:
GITHUB_TOKEN: ${{ github.token }}
# Website translations project
CROWDIN_PROJECT_ID: '4'
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
# As the files are extracted from a Docker container, they belong to root:root.
# We need to fix this before the next steps.
- name: Fix file permissions
run: sudo chown -R runner:docker .
- name: Compile website translations
id: compile_translations
run: |
npx nx run twenty-website-new:lingui:compile
git status
git add packages/twenty-website-new/src/locales
if ! git diff --staged --quiet --exit-code; then
git commit -m "chore: compile website translations"
echo "changes_detected=true" >> $GITHUB_OUTPUT
else
echo "changes_detected=false" >> $GITHUB_OUTPUT
fi
- name: Push changes
if: steps.compile_translations.outputs.changes_detected == 'true'
run: git push origin HEAD:i18n-website
- name: Create pull request
if: steps.compile_translations.outputs.changes_detected == 'true'
run: |
if git diff --name-only origin/main..HEAD | grep -q .; then
gh pr create -B main -H i18n-website --title 'i18n - website translations' --body 'Created by Github action' || true
else
echo "No file differences between branches, skipping PR creation"
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
+111
View File
@@ -0,0 +1,111 @@
name: 'Push website translations to Crowdin'
permissions:
contents: write
pull-requests: write
on:
workflow_dispatch:
workflow_call:
push:
branches: ['main']
paths:
- 'packages/twenty-website-new/**'
- '.github/crowdin-website.yml'
- '.github/workflows/website-i18n-push.yaml'
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
extract_website_translations:
name: Extract and upload website translations
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ github.token }}
ref: main
- name: Setup website i18n branch
run: |
git fetch origin i18n-website || true
git checkout -B i18n-website origin/i18n-website || git checkout -b i18n-website
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build dependencies
run: npx nx build twenty-shared
- name: Extract website translations
run: npx nx run twenty-website-new:lingui:extract
- name: Check and commit extracted files
id: check_extract_changes
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@twenty.com'
git add packages/twenty-website-new/src/locales
if ! git diff --staged --quiet --exit-code; then
git commit -m "chore: extract website translations"
echo "changes_detected=true" >> $GITHUB_OUTPUT
else
echo "changes_detected=false" >> $GITHUB_OUTPUT
fi
- name: Compile website translations
run: npx nx run twenty-website-new:lingui:compile
- name: Check and commit compiled files
id: check_compile_changes
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@twenty.com'
git add packages/twenty-website-new/src/locales/generated
if ! git diff --staged --quiet --exit-code; then
git commit -m "chore: compile website translations"
echo "changes_detected=true" >> $GITHUB_OUTPUT
else
echo "changes_detected=false" >> $GITHUB_OUTPUT
fi
- name: Push changes and create remote branch if needed
if: steps.check_extract_changes.outputs.changes_detected == 'true' || steps.check_compile_changes.outputs.changes_detected == 'true'
run: git push origin HEAD:i18n-website
- name: Upload missing website translations
if: steps.check_extract_changes.outputs.changes_detected == 'true'
uses: crowdin/github-action@v2
with:
upload_sources: true
upload_translations: true
download_translations: false
localization_branch_name: i18n-website
base_url: 'https://twenty.api.crowdin.com'
config: '.github/crowdin-website.yml'
env:
# Website translations project
CROWDIN_PROJECT_ID: '4'
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: Create a pull request
if: steps.check_extract_changes.outputs.changes_detected == 'true' || steps.check_compile_changes.outputs.changes_detected == 'true'
run: |
if git diff --name-only origin/main..HEAD | grep -q .; then
gh pr create -B main -H i18n-website --title 'i18n - website translations' --body 'Created by Github action' || true
else
echo "No file differences between branches, skipping PR creation"
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
+3 -155
View File
@@ -1,172 +1,19 @@
{
"private": true,
"dependencies": {
"@apollo/client": "^4.0.0",
"@floating-ui/react": "^0.24.3",
"@linaria/core": "^6.2.0",
"@linaria/react": "^6.2.1",
"@radix-ui/colors": "^3.0.0",
"@sniptt/guards": "^0.2.0",
"@tabler/icons-react": "^3.31.0",
"@wyw-in-js/babel-preset": "^1.0.6",
"@wyw-in-js/vite": "^0.7.0",
"archiver": "^7.0.1",
"danger-plugin-todos": "^1.3.1",
"date-fns": "^2.30.0",
"date-fns-tz": "^2.0.0",
"deep-equal": "^2.2.2",
"file-type": "16.5.4",
"framer-motion": "^11.18.0",
"fuse.js": "^7.1.0",
"googleapis": "105",
"hex-rgb": "^5.0.0",
"immer": "^10.1.1",
"jotai": "^2.17.1",
"libphonenumber-js": "^1.10.26",
"lodash.camelcase": "^4.3.0",
"lodash.chunk": "^4.2.0",
"lodash.compact": "^3.0.1",
"lodash.escaperegexp": "^4.1.2",
"lodash.groupby": "^4.6.0",
"lodash.identity": "^3.0.0",
"lodash.isempty": "^4.4.0",
"lodash.isequal": "^4.5.0",
"lodash.isobject": "^3.0.2",
"lodash.kebabcase": "^4.1.1",
"lodash.mapvalues": "^4.6.0",
"lodash.merge": "^4.6.2",
"lodash.omit": "^4.5.0",
"lodash.pickby": "^4.6.0",
"lodash.snakecase": "^4.1.1",
"lodash.upperfirst": "^4.3.1",
"microdiff": "^1.3.2",
"next-with-linaria": "^1.3.0",
"planer": "^1.2.0",
"pluralize": "^8.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-responsive": "^9.0.2",
"react-router-dom": "^6.30.3",
"react-tooltip": "^5.13.1",
"remark-gfm": "^4.0.1",
"rxjs": "^7.2.0",
"semver": "^7.5.4",
"slash": "^5.1.0",
"temporal-polyfill": "^0.3.0",
"ts-key-enum": "^2.0.12",
"tslib": "^2.8.1",
"type-fest": "4.10.1",
"typescript": "5.9.2",
"uuid": "^9.0.0",
"vite-tsconfig-paths": "^4.2.1",
"xlsx-ugnis": "^0.19.3",
"zod": "^4.1.11"
},
"devDependencies": {
"@babel/core": "^7.14.5",
"@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "^7.24.6",
"@chromatic-com/storybook": "^4.1.3",
"@graphql-codegen/cli": "^3.3.1",
"@graphql-codegen/client-preset": "^4.1.0",
"@graphql-codegen/typescript": "^3.0.4",
"@graphql-codegen/typescript-operations": "^3.0.4",
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
"@nx/jest": "22.5.4",
"@nx/js": "22.5.4",
"@nx/react": "22.5.4",
"@nx/storybook": "22.5.4",
"@nx/vite": "22.5.4",
"@nx/web": "22.5.4",
"@oxlint/plugins": "^1.51.0",
"@sentry/types": "^8",
"@storybook-community/storybook-addon-cookie": "^5.0.0",
"@storybook/addon-coverage": "^3.0.0",
"@storybook/addon-docs": "^10.3.3",
"@storybook/addon-links": "^10.3.3",
"@storybook/addon-vitest": "^10.3.3",
"@storybook/icons": "^2.0.1",
"@storybook/react-vite": "^10.3.3",
"@storybook/test-runner": "^0.24.2",
"@swc-node/register": "^1.11.1",
"@swc/cli": "^0.7.10",
"@swc/core": "^1.15.11",
"@swc/helpers": "~0.5.19",
"@swc/jest": "^0.2.39",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@types/addressparser": "^1.0.3",
"@types/bcrypt": "^5.0.0",
"@types/bytes": "^3.1.1",
"@types/chrome": "^0.0.267",
"@types/deep-equal": "^1.0.1",
"@types/fs-extra": "^11.0.4",
"@types/graphql-fields": "^1.3.6",
"@types/inquirer": "^9.0.9",
"@types/jest": "^30.0.0",
"@types/lodash.camelcase": "^4.3.7",
"@types/lodash.compact": "^3.0.9",
"@types/lodash.escaperegexp": "^4.1.9",
"@types/lodash.groupby": "^4.6.9",
"@types/lodash.identity": "^3.0.9",
"@types/lodash.isempty": "^4.4.7",
"@types/lodash.isequal": "^4.5.7",
"@types/lodash.isobject": "^3.0.7",
"@types/lodash.kebabcase": "^4.1.7",
"@types/lodash.mapvalues": "^4.6.9",
"@types/lodash.omit": "^4.5.9",
"@types/lodash.pickby": "^4.6.9",
"@types/lodash.snakecase": "^4.1.7",
"@types/lodash.upperfirst": "^4.3.7",
"@types/ms": "^0.7.31",
"@types/node": "^24.0.0",
"@types/passport-google-oauth20": "^2.0.11",
"@types/passport-jwt": "^3.0.8",
"@types/passport-microsoft": "^2.1.0",
"@types/pluralize": "^0.0.33",
"@types/react": "^18.2.39",
"@types/react-datepicker": "^6.2.0",
"@types/react-dom": "^18.2.15",
"@types/supertest": "^2.0.11",
"@types/uuid": "^9.0.2",
"@typescript/native-preview": "^7.0.0-dev.20260116.1",
"@vitejs/plugin-react-swc": "4.2.3",
"@vitest/browser-playwright": "^4.0.18",
"@vitest/coverage-istanbul": "^4.0.18",
"@vitest/coverage-v8": "^4.0.18",
"@yarnpkg/types": "^4.0.0",
"chromatic": "^6.18.0",
"concurrently": "^8.2.2",
"danger": "^13.0.4",
"dotenv-cli": "^7.4.4",
"esbuild": "^0.25.10",
"http-server": "^14.1.1",
"jest": "29.7.0",
"jest-environment-jsdom": "30.0.0-beta.3",
"jest-environment-node": "^29.4.1",
"jest-fetch-mock": "^3.0.3",
"jsdom": "~22.1.0",
"msw": "^2.12.7",
"msw-storybook-addon": "^2.0.6",
"nx": "22.5.4",
"prettier": "^3.1.1",
"raw-loader": "^4.0.2",
"rimraf": "^5.0.5",
"source-map-support": "^0.5.20",
"storybook": "^10.3.3",
"storybook-addon-mock-date": "2.0.0",
"storybook-addon-pseudo-states": "^10.3.3",
"supertest": "^6.1.3",
"ts-jest": "^29.1.1",
"ts-loader": "^9.2.3",
"ts-node": "10.9.1",
"tsc-alias": "^1.8.16",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.17.0",
"verdaccio": "^6.3.1",
"vite": "^7.0.0",
"vitest": "^4.0.18"
"verdaccio": "^6.3.1"
},
"engines": {
"node": "^24.5.0",
@@ -185,7 +32,8 @@
"@lingui/core": "5.1.2",
"@types/qs": "6.9.16",
"@wyw-in-js/transform@npm:0.6.0": "patch:@wyw-in-js/transform@npm%3A0.7.0#~/.yarn/patches/@wyw-in-js-transform-npm-0.7.0-ba641dc99f.patch",
"@wyw-in-js/transform@npm:0.7.0": "patch:@wyw-in-js/transform@npm%3A0.7.0#~/.yarn/patches/@wyw-in-js-transform-npm-0.7.0-ba641dc99f.patch"
"@wyw-in-js/transform@npm:0.7.0": "patch:@wyw-in-js/transform@npm%3A0.7.0#~/.yarn/patches/@wyw-in-js-transform-npm-0.7.0-ba641dc99f.patch",
"@opentelemetry/api": "1.9.1"
},
"version": "0.2.1",
"nx": {},
+6 -1
View File
@@ -1,6 +1,6 @@
{
"name": "create-twenty-app",
"version": "2.1.0",
"version": "2.3.0",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",
@@ -40,12 +40,17 @@
"uuid": "^13.0.0"
},
"devDependencies": {
"@swc/core": "^1.15.11",
"@swc/jest": "^0.2.39",
"@types/fs-extra": "^11.0.0",
"@types/inquirer": "^9.0.0",
"@types/jest": "^30.0.0",
"@types/lodash.camelcase": "^4.3.7",
"@types/lodash.kebabcase": "^4.1.7",
"@types/lodash.startcase": "^4",
"@types/node": "^20.0.0",
"jest": "29.7.0",
"jest-environment-node": "^29.4.1",
"twenty-shared": "workspace:*",
"typescript": "^5.9.2",
"vite": "^7.0.0",
+3
View File
@@ -26,6 +26,7 @@ const program = new Command(packageJson.name)
'--skip-local-instance',
'Skip the local Twenty instance setup prompt',
)
.option('-y, --yes', 'Auto-confirm prompts (e.g. start existing container)')
.helpOption('-h, --help', 'Display this help message.')
.action(
async (
@@ -36,6 +37,7 @@ const program = new Command(packageJson.name)
displayName?: string;
description?: string;
skipLocalInstance?: boolean;
yes?: boolean;
},
) => {
if (directory && !/^[a-z0-9-]+$/.test(directory)) {
@@ -59,6 +61,7 @@ const program = new Command(packageJson.name)
displayName: options?.displayName,
description: options?.description,
skipLocalInstance: options?.skipLocalInstance,
yes: options?.yes,
});
},
);
@@ -11,7 +11,9 @@ import * as path from 'path';
import { basename } from 'path';
import {
authLoginOAuth,
checkDockerRunning,
ConfigService,
containerExists,
detectLocalServer,
serverStart,
type ServerStartResult,
@@ -27,6 +29,7 @@ type CreateAppOptions = {
displayName?: string;
description?: string;
skipLocalInstance?: boolean;
yes?: boolean;
};
export class CreateAppCommand {
@@ -71,7 +74,7 @@ export class CreateAppCommand {
let serverResult: ServerStartResult | undefined;
if (!options.skipLocalInstance) {
const shouldStartServer = await this.shouldStartServer();
const shouldStartServer = await this.shouldStartServer(options.yes);
if (shouldStartServer) {
const startResult = await serverStart({
@@ -223,13 +226,35 @@ export class CreateAppCommand {
);
}
private async shouldStartServer(): Promise<boolean> {
private async shouldStartServer(autoConfirm?: boolean): Promise<boolean> {
const existingServerUrl = await detectLocalServer();
if (existingServerUrl) {
return true;
}
if (checkDockerRunning() && containerExists()) {
if (autoConfirm) {
return true;
}
const { startExisting } = await inquirer.prompt([
{
type: 'confirm',
name: 'startExisting',
message:
'An existing Twenty server container was found. Would you like to start it?',
default: true,
},
]);
return startExisting;
}
if (autoConfirm) {
return true;
}
const { startDocker } = await inquirer.prompt([
{
type: 'confirm',
@@ -8,7 +8,6 @@ export default defineApplication({
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
displayName: 'Postcard App',
description: 'Send postcards easily with Twenty',
icon: 'IconWorld',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
@@ -4,6 +4,5 @@ export default defineApplication({
universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000001',
displayName: 'Root App',
description: 'An app with all entities at root level',
icon: 'IconFolder',
defaultRoleUniversalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000002',
});
@@ -5,7 +5,6 @@ export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
displayName: 'Rich App',
description: 'A simple rich app',
icon: 'IconWorld',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
@@ -30,31 +30,31 @@ export default defineView({
],
groups: [
{
universalIdentifier: 'bg1a2b3c-0001-4a7b-8c9d-0e1f2a3b4c5d',
universalIdentifier: 'e9ed34f1-3c3d-41b1-869b-00aae0033d9c',
fieldValue: 'DRAFT',
position: 0,
isVisible: true,
},
{
universalIdentifier: 'bg1a2b3c-0002-4a7b-8c9d-0e1f2a3b4c5d',
universalIdentifier: '19b1a3c1-53f0-4d32-b072-d645dac98e38',
fieldValue: 'SENT',
position: 1,
isVisible: true,
},
{
universalIdentifier: 'bg1a2b3c-0003-4a7b-8c9d-0e1f2a3b4c5d',
universalIdentifier: 'f545cb5a-370d-423f-9b4e-278a9a465bdf',
fieldValue: 'DELIVERED',
position: 2,
isVisible: true,
},
{
universalIdentifier: 'bg1a2b3c-0004-4a7b-8c9d-0e1f2a3b4c5d',
universalIdentifier: '5d4c6d5f-af53-4cd0-a843-df38915561b2',
fieldValue: 'RETURNED',
position: 3,
isVisible: true,
},
{
universalIdentifier: 'bg1a2b3c-0005-4a7b-8c9d-0e1f2a3b4c5d',
universalIdentifier: '5ebbd7dc-9939-4594-b2a0-519269b4531f',
fieldValue: 'LOST',
position: 4,
isVisible: true,
@@ -0,0 +1,19 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript"],
"categories": {
"correctness": "off"
},
"ignorePatterns": ["node_modules", "dist"],
"rules": {
"no-unused-vars": "off",
"typescript/no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_"
}
],
"typescript/no-explicit-any": "off"
}
}
@@ -0,0 +1,79 @@
# Linear for Twenty
Connect your Linear account to Twenty to create issues and look up teams
straight from your workflows or the AI chat.
## What you can do
Once installed and connected, two tools become available:
- **Create Linear issue** — from the AI chat, ask something like
*"create a Linear issue in the Engineering team titled 'Fix login bug'"*
and the AI will file it for you. From a workflow, add it as a step
with `teamId` + `title` (and optional `description`).
- **List Linear teams** — discovers the teams in your Linear workspace,
useful when you need to pick a `teamId` for the create-issue step.
## Installing
1. Open **Settings → Applications** in your Twenty workspace.
2. Find **Linear** in the available apps and click **Install**.
3. Open the app, go to the **Connections** tab, and click **Add connection**.
4. Choose **Just for me** (your personal Linear account) or
**Workspace shared** (a team-managed Linear account anyone in this
workspace can act through), then complete the Linear sign-in.
That's it — you can now use the tools above.
> If you see a "Linear OAuth is not yet set up by your server administrator"
> notice on the Connections tab, ask your Twenty admin to follow the
> **Self-hosting setup** below — they need to provide the OAuth credentials
> before connections can be added.
---
## Self-hosting setup
This section is for Twenty server admins. If you're on Twenty Cloud, skip
this — the OAuth credentials are already configured.
### 1. Register an OAuth app in Linear
1. Visit https://linear.app/settings/api/applications/new.
2. Set the **Redirect URI** to `<SERVER_URL>/apps/oauth/callback` (for
local dev: `http://localhost:3000/apps/oauth/callback`).
3. Copy the generated **Client ID** and **Client Secret**.
### 2. Wire the credentials into Twenty
1. In **Settings → Applications**, find **Linear**, click into it, and go
to the **Application registration** tab (admin-only).
2. Paste your Linear **Client ID** into `LINEAR_CLIENT_ID` and the
**Client Secret** into `LINEAR_CLIENT_SECRET`.
Workspace users will now be able to add Linear connections from the
**Connections** tab as described above.
### 3. (Developers only) Building the app from source
If you're working on this app rather than installing the published version:
```bash
cd packages/twenty-apps/internal/twenty-linear
# For day-to-day development (publish + install + watch in one):
yarn twenty dev
# Manual publish flow (deploy registers the app, install activates it):
yarn twenty deploy
yarn twenty install
```
`twenty dev` is recommended for iteration — it publishes, installs, and
watches for changes in one command. Use `twenty deploy` + `twenty install`
when you want to control each step separately (e.g. deploying to a
production server without auto-installing).
This serves as the reference implementation for Twenty's
`defineConnectionProvider({ type: 'oauth' })` flow — useful as a template
when adding OAuth integrations for other providers.
@@ -0,0 +1,32 @@
{
"name": "twenty-linear",
"version": "0.1.5",
"description": "Linear integration for Twenty. Connect a user's Linear account and create issues from logic functions.",
"license": "MIT",
"engines": {
"node": "^24.5.0",
"npm": "please-use-yarn",
"yarn": ">=4.0.2"
},
"keywords": [
"twenty-app"
],
"packageManager": "yarn@4.9.2",
"scripts": {
"twenty": "twenty",
"lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint --fix -c .oxlintrc.json .",
"test": "vitest run --config vitest.unit.config.ts",
"test:watch": "vitest --config vitest.unit.config.ts"
},
"dependencies": {
"twenty-sdk": "2.1.0"
},
"devDependencies": {
"@types/node": "^24.7.2",
"oxlint": "^0.16.0",
"typescript": "^5.9.3",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^3.1.1"
}
}
@@ -0,0 +1 @@
<svg fill="#5E6AD2" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Linear</title><path d="M2.886 4.18A11.982 11.982 0 0 1 11.99 0C18.624 0 24 5.376 24 12.009c0 3.64-1.62 6.903-4.18 9.105L2.887 4.18ZM1.817 5.626l16.556 16.556c-.524.33-1.075.62-1.65.866L.951 7.277c.247-.575.537-1.126.866-1.65ZM.322 9.163l14.515 14.515c-.71.172-1.443.282-2.195.322L0 11.358a12 12 0 0 1 .322-2.195Zm-.17 4.862 9.823 9.824a12.02 12.02 0 0 1-9.824-9.824Z"/></svg>

After

Width:  |  Height:  |  Size: 469 B

@@ -0,0 +1,32 @@
import { defineApplication } from 'twenty-sdk/define';
import {
APPLICATION_UNIVERSAL_IDENTIFIER,
DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
export default defineApplication({
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
displayName: 'Linear',
description:
'Connect Linear to Twenty. Each workspace member connects their own Linear account; logic functions can then create issues and read team data on their behalf.',
logoUrl: 'public/linear-logomark.svg',
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
// OAuth client_id/secret live at the registration level (one OAuth app per
// Twenty server, configured by the server admin) — not per-workspace —
// so they're declared as serverVariables, not applicationVariables.
serverVariables: {
LINEAR_CLIENT_ID: {
description:
'OAuth client ID from your Linear OAuth application (linear.app/settings/api/applications).',
isSecret: false,
isRequired: true,
},
LINEAR_CLIENT_SECRET: {
description:
'OAuth client secret from your Linear OAuth application. Stored encrypted; never exposed in API responses.',
isSecret: true,
isRequired: true,
},
},
});
@@ -0,0 +1,22 @@
import { defineConnectionProvider } from 'twenty-sdk/define';
import { LINEAR_CONNECTION_PROVIDER_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
export default defineConnectionProvider({
universalIdentifier: LINEAR_CONNECTION_PROVIDER_UNIVERSAL_IDENTIFIER,
name: 'linear',
displayName: 'Linear',
type: 'oauth',
oauth: {
authorizationEndpoint: 'https://linear.app/oauth/authorize',
tokenEndpoint: 'https://api.linear.app/oauth/token',
revokeEndpoint: 'https://api.linear.app/oauth/revoke',
scopes: ['read', 'write'],
clientIdVariable: 'LINEAR_CLIENT_ID',
clientSecretVariable: 'LINEAR_CLIENT_SECRET',
tokenRequestContentType: 'form-urlencoded',
// Linear supports PKCE but doesn't require it for confidential clients.
// Disabled to keep the test surface minimal.
usePkce: false,
},
});
@@ -0,0 +1,19 @@
// Group all universal identifiers in a single file. Per the codebase
// convention (see twenty-for-twenty), closely-related constants live
// together so the rest of the app's source files can stay at one
// `export default` per file.
export const APPLICATION_UNIVERSAL_IDENTIFIER =
'6f4e7c2a-3d8e-4a91-b2cf-9e0b8d5f4a2e';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b6c33347-e41c-4b90-8a37-7b3c49baa85a';
export const LINEAR_CONNECTION_PROVIDER_UNIVERSAL_IDENTIFIER =
'9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f';
export const CREATE_LINEAR_ISSUE_UNIVERSAL_IDENTIFIER =
'01f829c9-1661-41fa-9ee1-b67e64716c2e';
export const LIST_LINEAR_TEAMS_UNIVERSAL_IDENTIFIER =
'15824bbc-9c64-4f97-b45f-d0a44b402bb8';
@@ -0,0 +1,126 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { createLinearIssueHandler } from '../handlers/create-linear-issue-handler';
import { buildConnection, stubConnectionsThenLinear } from './test-utils';
const SAVED_ENV = { ...process.env };
describe('createLinearIssueHandler', () => {
beforeEach(() => {
process.env.TWENTY_API_URL = 'http://api.test';
process.env.TWENTY_APP_ACCESS_TOKEN = 'app-token';
});
afterEach(() => {
process.env = { ...SAVED_ENV };
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
it('returns an error when required input fields are missing', async () => {
const result = await createLinearIssueHandler({ title: 'no team' });
expect(result).toMatchObject({
success: false,
error: expect.stringContaining('teamId'),
});
});
it('returns an error when no Linear connection exists', async () => {
stubConnectionsThenLinear([], {});
const result = await createLinearIssueHandler({
teamId: 'team_1',
title: 'hi',
});
expect(result).toMatchObject({
success: false,
error: expect.stringContaining('not connected'),
});
});
it('calls Linear with the only available connection and returns the issue', async () => {
const issue = {
id: 'issue_1',
identifier: 'TEAM-1',
title: 'Hello from Twenty',
url: 'https://linear.app/twenty/issue/TEAM-1',
};
const fetchMock = stubConnectionsThenLinear([buildConnection()], {
data: { issueCreate: { success: true, issue } },
});
const result = await createLinearIssueHandler({
teamId: 'team_1',
title: 'Hello from Twenty',
description: 'Body',
});
expect(result).toEqual({ success: true, issue });
const [url, init] = fetchMock.mock.calls[1];
expect(url).toBe('https://api.linear.app/graphql');
expect(init.headers.Authorization).toBe('Bearer lin_test_access_token');
expect(JSON.parse(init.body as string).variables.input).toEqual({
teamId: 'team_1',
title: 'Hello from Twenty',
description: 'Body',
});
});
it('prefers a workspace-shared connection over a user-visibility one', async () => {
const userConnection = buildConnection({
id: 'conn_user',
accessToken: 'lin_user',
});
const sharedConnection = buildConnection({
id: 'conn_shared',
visibility: 'workspace',
accessToken: 'lin_shared',
});
const fetchMock = stubConnectionsThenLinear(
[userConnection, sharedConnection],
{
data: {
issueCreate: {
success: true,
issue: {
id: 'issue_2',
identifier: 'T-2',
title: 'Hi',
url: 'https://linear.app/x/T-2',
},
},
},
},
);
const result = await createLinearIssueHandler({
teamId: 'team_1',
title: 'Hi',
});
expect(result.success).toBe(true);
expect(fetchMock.mock.calls[1][1].headers.Authorization).toBe(
'Bearer lin_shared',
);
});
it('surfaces Linear GraphQL errors as the handler error', async () => {
stubConnectionsThenLinear([buildConnection()], {
errors: [{ message: 'Invalid teamId' }],
});
const result = await createLinearIssueHandler({
teamId: 'bogus',
title: 'hi',
});
expect(result).toEqual({ success: false, error: 'Invalid teamId' });
});
});
@@ -0,0 +1,56 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { listLinearTeamsHandler } from '../handlers/list-linear-teams-handler';
import { buildConnection, stubConnectionsThenLinear } from './test-utils';
const SAVED_ENV = { ...process.env };
describe('listLinearTeamsHandler', () => {
beforeEach(() => {
process.env.TWENTY_API_URL = 'http://api.test';
process.env.TWENTY_APP_ACCESS_TOKEN = 'app-token';
});
afterEach(() => {
process.env = { ...SAVED_ENV };
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
it('returns the teams when the Linear query succeeds', async () => {
const teams = [
{ id: 'team_1', name: 'Engineering', key: 'ENG' },
{ id: 'team_2', name: 'Design', key: 'DES' },
];
const fetchMock = stubConnectionsThenLinear([buildConnection()], {
data: { teams: { nodes: teams } },
});
const result = await listLinearTeamsHandler();
expect(result).toEqual({ success: true, teams });
expect(fetchMock.mock.calls[1][1].headers.Authorization).toBe(
'Bearer lin_test_access_token',
);
});
it('returns success=false when no Linear connection exists', async () => {
stubConnectionsThenLinear([], { data: { teams: { nodes: [] } } });
const result = await listLinearTeamsHandler();
expect(result.success).toBe(false);
});
it('surfaces Linear errors', async () => {
stubConnectionsThenLinear([buildConnection()], {
errors: [{ message: 'rate limited' }],
});
const result = await listLinearTeamsHandler();
expect(result).toEqual({ success: false, error: 'rate limited' });
});
});
@@ -0,0 +1,48 @@
import { vi } from 'vitest';
export const USER_WORKSPACE_ID = '11111111-1111-1111-1111-111111111111';
export const buildConnection = (
overrides: Partial<Record<string, unknown>> = {},
) => ({
id: 'conn_1',
name: 'octocat@example.com',
visibility: 'user' as const,
providerName: 'linear',
userWorkspaceId: USER_WORKSPACE_ID,
accessToken: 'lin_test_access_token',
scopes: ['read', 'write'],
handle: 'octocat@example.com',
lastRefreshedAt: '2024-01-01T00:00:00.000Z',
authFailedAt: null,
...overrides,
});
// Stubs `fetch` to first answer the SDK's `/apps/connections/list` call, then
// the handler's downstream Linear GraphQL request.
export const stubConnectionsThenLinear = (
connections: ReturnType<typeof buildConnection>[],
linearJson: unknown,
) => {
const fetchMock = vi.fn(async (url: string) => {
if (url.endsWith('/apps/connections/list')) {
return {
ok: true,
status: 200,
json: async () => connections,
text: async () => JSON.stringify(connections),
};
}
return {
ok: true,
status: 200,
json: async () => linearJson,
text: async () => JSON.stringify(linearJson),
};
});
vi.stubGlobal('fetch', fetchMock);
return fetchMock;
};
@@ -0,0 +1,8 @@
export const ISSUE_CREATE_MUTATION = `
mutation IssueCreate($input: IssueCreateInput!) {
issueCreate(input: $input) {
success
issue { id identifier title url }
}
}
`;
@@ -0,0 +1,33 @@
import { defineLogicFunction } from 'twenty-sdk/define';
import { CREATE_LINEAR_ISSUE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
import { createLinearIssueHandler } from 'src/logic-functions/handlers/create-linear-issue-handler';
export default defineLogicFunction({
universalIdentifier: CREATE_LINEAR_ISSUE_UNIVERSAL_IDENTIFIER,
name: 'create-linear-issue',
description:
'Create a Linear issue on behalf of the connected user. Requires a teamId (call list-linear-teams to discover one) and a title.',
timeoutSeconds: 30,
handler: createLinearIssueHandler,
isTool: true,
toolInputSchema: {
type: 'object',
properties: {
teamId: {
type: 'string',
description:
'The Linear team ID to create the issue in. Use list-linear-teams to discover available teams.',
},
title: {
type: 'string',
description: 'The issue title.',
},
description: {
type: 'string',
description: 'Optional issue description (Markdown supported).',
},
},
required: ['teamId', 'title'],
},
});
@@ -0,0 +1,73 @@
import { listConnections } from 'twenty-sdk/logic-function';
import { ISSUE_CREATE_MUTATION } from 'src/logic-functions/constants/issue-create-mutation.constant';
import { type CreateIssueInput } from 'src/logic-functions/types/create-issue-input.type';
import { type CreateIssueMutationResult } from 'src/logic-functions/types/create-issue-mutation-result.type';
import { callLinearGraphQL } from 'src/logic-functions/utils/call-linear-graphql';
type HandlerResult =
| {
success: true;
issue: {
id: string;
identifier: string;
title: string;
url: string;
};
}
| { success: false; error: string };
export const createLinearIssueHandler = async (
input: CreateIssueInput,
): Promise<HandlerResult> => {
if (!input.teamId || !input.title) {
return {
success: false,
error: 'Both `teamId` and `title` are required.',
};
}
const connections = await listConnections({ providerName: 'linear' });
// Workspace-shared credentials win when present (a team-managed service
// account); otherwise fall back to the first user-scoped connection.
const connection =
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
if (!connection) {
return {
success: false,
error:
'Linear is not connected. Open the app settings and click "Add connection" first.',
};
}
const result = await callLinearGraphQL<CreateIssueMutationResult>({
accessToken: connection.accessToken,
query: ISSUE_CREATE_MUTATION,
variables: {
input: {
teamId: input.teamId,
title: input.title,
description: input.description,
},
},
});
if (result.errors || !result.data) {
return {
success: false,
error: result.errors?.[0]?.message ?? 'Unknown Linear API error',
};
}
const { success, issue } = result.data.issueCreate;
if (!success || !issue) {
return {
success: false,
error: 'Linear reported the mutation as unsuccessful.',
};
}
return { success: true, issue };
};
@@ -0,0 +1,49 @@
import { listConnections } from 'twenty-sdk/logic-function';
import { callLinearGraphQL } from 'src/logic-functions/utils/call-linear-graphql';
type LinearTeam = {
id: string;
name: string;
key: string;
};
type TeamsQueryResult = {
teams: { nodes: LinearTeam[] };
};
type HandlerResult =
| { success: true; teams: LinearTeam[] }
| { success: false; error: string };
export const listLinearTeamsHandler = async (): Promise<HandlerResult> => {
const connections = await listConnections({ providerName: 'linear' });
const connection =
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
if (!connection) {
return {
success: false,
error:
'Linear is not connected. Open the app settings and click "Add connection" first.',
};
}
const result = await callLinearGraphQL<TeamsQueryResult>({
accessToken: connection.accessToken,
query: `
query Teams {
teams { nodes { id name key } }
}
`,
});
if (result.errors || !result.data) {
return {
success: false,
error: result.errors?.[0]?.message ?? 'Unknown Linear API error',
};
}
return { success: true, teams: result.data.teams.nodes };
};
@@ -0,0 +1,18 @@
import { defineLogicFunction } from 'twenty-sdk/define';
import { LIST_LINEAR_TEAMS_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
import { listLinearTeamsHandler } from 'src/logic-functions/handlers/list-linear-teams-handler';
export default defineLogicFunction({
universalIdentifier: LIST_LINEAR_TEAMS_UNIVERSAL_IDENTIFIER,
name: 'list-linear-teams',
description:
"Returns the connected user's Linear teams. Useful for picking a teamId to pass to create-linear-issue.",
timeoutSeconds: 15,
handler: listLinearTeamsHandler,
isTool: true,
toolInputSchema: {
type: 'object',
properties: {},
},
});
@@ -0,0 +1,5 @@
export type CreateIssueInput = {
teamId?: string;
title?: string;
description?: string;
};
@@ -0,0 +1,11 @@
export type CreateIssueMutationResult = {
issueCreate: {
success: boolean;
issue: {
id: string;
identifier: string;
title: string;
url: string;
} | null;
};
};
@@ -0,0 +1,58 @@
import { type LinearGraphQLResult } from 'src/logic-functions/utils/types/linear-graphql-result.type';
const LINEAR_GRAPHQL_ENDPOINT = 'https://api.linear.app/graphql';
export const callLinearGraphQL = async <TData>({
accessToken,
query,
variables,
}: {
accessToken: string;
query: string;
variables?: Record<string, unknown>;
}): Promise<LinearGraphQLResult<TData>> => {
let response: Response;
try {
response = await fetch(LINEAR_GRAPHQL_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({ query, variables }),
});
} catch (error) {
return {
errors: [
{
message: `Linear API request failed: ${(error as Error).message}`,
},
],
};
}
if (!response.ok) {
const text = await response.text().catch(() => '');
return {
errors: [
{
message: `Linear API responded with ${response.status}: ${text.slice(0, 500)}`,
},
],
};
}
try {
return (await response.json()) as LinearGraphQLResult<TData>;
} catch (error) {
return {
errors: [
{
message: `Linear API returned a non-JSON response: ${(error as Error).message}`,
},
],
};
}
};
@@ -0,0 +1,4 @@
export type LinearGraphQLResult<TData> = {
data?: TData;
errors?: Array<{ message: string }>;
};
@@ -0,0 +1,22 @@
import { defineRole } from 'twenty-sdk/define';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
// The Linear logic functions never read workspace data — they only call
// Linear's GraphQL API on behalf of the connected user.
export default defineRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Linear function role',
description: 'No-op role for Linear logic functions',
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
objectPermissions: [],
fieldPermissions: [],
permissionFlags: [],
});
@@ -0,0 +1,30 @@
{
"compileOnSave": false,
"compilerOptions": {
"sourceMap": true,
"declaration": true,
"outDir": "./dist",
"rootDir": ".",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"importHelpers": true,
"allowUnreachableCode": false,
"strict": true,
"alwaysStrict": true,
"noImplicitAny": true,
"strictBindCallApply": false,
"target": "es2020",
"module": "esnext",
"lib": ["es2020"],
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"resolveJsonModule": true,
"paths": {
"src/*": ["./src/*"],
"~/*": ["./*"]
}
},
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
}
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"composite": true,
"types": ["vitest/globals", "node"]
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}
@@ -0,0 +1,43 @@
import path from 'node:path';
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';
const TWENTY_SDK_SRC = path.resolve(
__dirname,
'../../../twenty-sdk/src/sdk',
);
// twenty-sdk's `exports` map points at compiled `./dist/*`. Aliasing the
// subpaths to source keeps unit tests self-contained — no `yarn build` in
// twenty-sdk required before running them.
export default defineConfig({
plugins: [
tsconfigPaths({
projects: ['tsconfig.spec.json'],
ignoreConfigErrors: true,
}),
],
resolve: {
alias: [
{
find: 'twenty-sdk/logic-function',
replacement: path.join(TWENTY_SDK_SRC, 'logic-function/index.ts'),
},
{
find: 'twenty-sdk/define',
replacement: path.join(TWENTY_SDK_SRC, 'define/index.ts'),
},
// The SDK source uses `@/*` to refer to its own `src/`. Vitest
// doesn't pick up the SDK's tsconfig path mapping when resolving
// a different package, so map the alias here.
{
find: /^@\/(.*)$/,
replacement: path.resolve(__dirname, '../../../twenty-sdk/src/$1'),
},
],
},
test: {
include: ['src/**/*.test.ts'],
},
});
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "twenty-client-sdk",
"version": "2.1.0",
"version": "2.3.0",
"sideEffects": false,
"license": "AGPL-3.0",
"scripts": {
@@ -46,6 +46,8 @@
"graphql": "^16.8.1"
},
"devDependencies": {
"@typescript/native-preview": "^7.0.0-dev.20260116.1",
"tsc-alias": "^1.8.16",
"twenty-shared": "workspace:*",
"typescript": "^5.9.2",
"vite": "^7.0.0",
@@ -580,6 +580,7 @@ type Application {
id: UUID!
name: String!
description: String
logo: String
version: String
universalIdentifier: String!
packageJsonChecksum: String
@@ -862,7 +863,6 @@ type User {
firstName: String!
lastName: String!
email: String!
defaultAvatarUrl: String
isEmailVerified: Boolean!
disabled: Boolean
canImpersonate: Boolean!
@@ -1292,6 +1292,20 @@ enum PageLayoutType {
STANDALONE_PAGE
}
type ApplicationConnectionProviderOAuthConfig {
scopes: [String!]!
isClientCredentialsConfigured: Boolean!
}
type ApplicationConnectionProvider {
id: UUID!
applicationId: String!
type: String!
name: String!
displayName: String!
oauth: ApplicationConnectionProviderOAuthConfig
}
type Analytics {
"""Boolean that confirms query was dispatched"""
success: Boolean!
@@ -2202,7 +2216,6 @@ type MarketplaceApp {
id: String!
name: String!
description: String!
icon: String!
author: String!
category: String!
logo: String
@@ -2537,6 +2550,10 @@ type ConnectedAccountDTO {
connectionParameters: ImapSmtpCaldavConnectionParameters
lastSignedInAt: DateTime
userWorkspaceId: UUID!
applicationConnectionProviderId: UUID
applicationId: UUID
name: String
visibility: String!
createdAt: DateTime!
updatedAt: DateTime!
}
@@ -2564,6 +2581,10 @@ type ConnectedAccountPublicDTO {
scopes: [String!]
lastSignedInAt: DateTime
userWorkspaceId: UUID!
applicationConnectionProviderId: UUID
applicationId: UUID
name: String
visibility: String!
createdAt: DateTime!
updatedAt: DateTime!
connectionParameters: PublicImapSmtpCaldavConnectionParameters
@@ -2609,19 +2630,6 @@ type Skill {
updatedAt: DateTime!
}
type AgentChatThread {
id: UUID!
title: String
totalInputTokens: Int!
totalOutputTokens: Int!
contextWindowTokens: Int
conversationSize: Int!
totalInputCredits: Float!
totalOutputCredits: Float!
createdAt: DateTime!
updatedAt: DateTime!
}
type AgentMessage {
id: UUID!
threadId: UUID!
@@ -2634,6 +2642,21 @@ type AgentMessage {
createdAt: DateTime!
}
type AgentChatThread {
id: ID!
title: String
totalInputTokens: Int!
totalOutputTokens: Int!
contextWindowTokens: Int
conversationSize: Int!
totalInputCredits: Float!
totalOutputCredits: Float!
createdAt: DateTime!
updatedAt: DateTime!
deletedAt: DateTime
lastMessageAt: DateTime
}
type AiSystemPromptSection {
title: String!
content: String!
@@ -2661,22 +2684,6 @@ type AgentChatEvent {
event: JSON!
}
type AgentChatThreadEdge {
"""The node containing the AgentChatThread"""
node: AgentChatThread!
"""Cursor for this node."""
cursor: ConnectionCursor!
}
type AgentChatThreadConnection {
"""Paging information"""
pageInfo: PageInfo!
"""Array of edges."""
edges: [AgentChatThreadEdge!]!
}
type AgentTurnEvaluation {
id: UUID!
turnId: UUID!
@@ -2933,6 +2940,7 @@ type Query {
getPageLayoutTab(id: String!): PageLayoutTab!
getPageLayouts(objectMetadataId: String, pageLayoutType: PageLayoutType): [PageLayout!]!
getPageLayout(id: String!): PageLayout
applicationConnectionProviders(applicationId: UUID!): [ApplicationConnectionProvider!]!
getPageLayoutWidgets(pageLayoutTabId: String!): [PageLayoutWidget!]!
getPageLayoutWidget(id: String!): PageLayoutWidget!
findOneLogicFunction(input: LogicFunctionIdInput!): LogicFunction!
@@ -2993,22 +3001,13 @@ type Query {
webhooks: [Webhook!]!
webhook(id: UUID!): Webhook
minimalMetadata: MinimalMetadata!
chatThreads: [AgentChatThread!]!
chatThread(id: UUID!): AgentChatThread!
chatMessages(threadId: UUID!): [AgentMessage!]!
chatStreamCatchupChunks(threadId: UUID!): ChatStreamCatchupChunks!
getAiSystemPromptPreview: AiSystemPromptPreview!
skills: [Skill!]!
skill(id: UUID!): Skill
chatThreads(
"""Limit or page results."""
paging: CursorPaging! = {first: 10}
"""Specify to filter the records returned."""
filter: AgentChatThreadFilter! = {}
"""Specify to sort results."""
sorting: [AgentChatThreadSort!]! = [{field: updatedAt, direction: DESC}]
): AgentChatThreadConnection!
agentTurns(agentId: UUID!): [AgentTurn!]!
checkUserExists(email: String!, captchaToken: String): CheckUserExist!
checkWorkspaceInviteHashIsValid(inviteHash: String!): WorkspaceInviteHashValid!
@@ -3057,56 +3056,6 @@ input AgentIdInput {
id: UUID!
}
input AgentChatThreadFilter {
and: [AgentChatThreadFilter!]
or: [AgentChatThreadFilter!]
id: UUIDFilterComparison
updatedAt: DateFieldComparison
}
input DateFieldComparison {
is: Boolean
isNot: Boolean
eq: DateTime
neq: DateTime
gt: DateTime
gte: DateTime
lt: DateTime
lte: DateTime
in: [DateTime!]
notIn: [DateTime!]
between: DateFieldComparisonBetween
notBetween: DateFieldComparisonBetween
}
input DateFieldComparisonBetween {
lower: DateTime!
upper: DateTime!
}
input AgentChatThreadSort {
field: AgentChatThreadSortFields!
direction: SortDirection!
nulls: SortNulls
}
enum AgentChatThreadSortFields {
id
updatedAt
}
"""Sort Directions"""
enum SortDirection {
ASC
DESC
}
"""Sort Nulls Options"""
enum SortNulls {
NULLS_FIRST
NULLS_LAST
}
input EventLogQueryInput {
table: EventLogTable!
filters: EventLogFiltersInput
@@ -3193,6 +3142,7 @@ type Mutation {
updateView(id: String!, input: UpdateViewInput!): View!
deleteView(id: String!): Boolean!
destroyView(id: String!): Boolean!
upsertViewWidget(input: UpsertViewWidgetInput!): View!
createViewSort(input: CreateViewSortInput!): ViewSort!
updateViewSort(input: UpdateViewSortInput!): ViewSort!
deleteViewSort(input: DeleteViewSortInput!): Boolean!
@@ -3242,6 +3192,7 @@ type Mutation {
resetPageLayoutToDefault(id: String!): PageLayout!
resetPageLayoutWidgetToDefault(id: String!): PageLayoutWidget!
resetPageLayoutTabToDefault(id: String!): PageLayoutTab!
updateOneApplicationVariable(key: String!, value: String!, applicationId: UUID!): Boolean!
createPageLayoutWidget(input: CreatePageLayoutWidgetInput!): PageLayoutWidget!
updatePageLayoutWidget(id: String!, input: UpdatePageLayoutWidgetInput!): PageLayoutWidget!
destroyPageLayoutWidget(id: String!): Boolean!
@@ -3291,6 +3242,10 @@ type Mutation {
createChatThread: AgentChatThread!
sendChatMessage(threadId: UUID!, text: String!, messageId: UUID!, browsingContext: JSON, modelId: String, fileIds: [UUID!]): SendChatMessageResult!
stopAgentChatStream(threadId: UUID!): Boolean!
renameChatThread(id: UUID!, title: String!): AgentChatThread!
archiveChatThread(id: UUID!): AgentChatThread!
unarchiveChatThread(id: UUID!): AgentChatThread!
deleteChatThread(id: UUID!): Boolean!
deleteQueuedChatMessage(messageId: UUID!): Boolean!
createSkill(input: CreateSkillInput!): Skill!
updateSkill(input: UpdateSkillInput!): Skill!
@@ -3340,7 +3295,6 @@ type Mutation {
installApplication(appRegistrationId: String!, version: String): Boolean!
runWorkspaceMigration(workspaceMigration: WorkspaceMigrationInput!): Boolean!
uninstallApplication(universalIdentifier: String!): Boolean!
updateOneApplicationVariable(key: String!, value: String!, applicationId: UUID!): Boolean!
createOIDCIdentityProvider(input: SetupOIDCSsoInput!): SetupSso!
createSAMLIdentityProvider(input: SetupSAMLSsoInput!): SetupSso!
deleteSSOIdentityProvider(input: DeleteSsoInput!): DeleteSso!
@@ -3510,6 +3464,59 @@ input UpdateViewInput {
shouldHideEmptyGroups: Boolean
}
input UpsertViewWidgetInput {
"""The id of the view widget (page layout widget)."""
widgetId: UUID!
"""The view fields to upsert."""
viewFields: [UpsertViewWidgetViewFieldInput!]
"""The view filters to upsert."""
viewFilters: [UpsertViewWidgetViewFilterInput!]
"""The view filter groups to upsert."""
viewFilterGroups: [UpsertViewWidgetViewFilterGroupInput!]
"""The view sorts to upsert."""
viewSorts: [UpsertViewWidgetViewSortInput!]
}
input UpsertViewWidgetViewFieldInput {
"""The id of an existing view field to update."""
viewFieldId: UUID
"""
The field metadata id. Used to create a new view field when viewFieldId is not provided.
"""
fieldMetadataId: UUID
isVisible: Boolean!
position: Float!
size: Float
}
input UpsertViewWidgetViewFilterInput {
id: UUID
fieldMetadataId: UUID!
operand: ViewFilterOperand = CONTAINS
value: JSON!
viewFilterGroupId: UUID
positionInViewFilterGroup: Float
subFieldName: String
}
input UpsertViewWidgetViewFilterGroupInput {
id: UUID
parentViewFilterGroupId: UUID
logicalOperator: ViewFilterGroupLogicalOperator = AND
positionInViewFilterGroup: Float
}
input UpsertViewWidgetViewSortInput {
id: UUID
fieldMetadataId: UUID!
direction: ViewSortDirection = ASC
}
input CreateViewSortInput {
id: UUID
fieldMetadataId: UUID!
@@ -414,6 +414,7 @@ export interface Application {
id: Scalars['UUID']
name: Scalars['String']
description?: Scalars['String']
logo?: Scalars['String']
version?: Scalars['String']
universalIdentifier: Scalars['String']
packageJsonChecksum?: Scalars['String']
@@ -647,7 +648,6 @@ export interface User {
firstName: Scalars['String']
lastName: Scalars['String']
email: Scalars['String']
defaultAvatarUrl?: Scalars['String']
isEmailVerified: Scalars['Boolean']
disabled?: Scalars['Boolean']
canImpersonate: Scalars['Boolean']
@@ -1018,6 +1018,22 @@ export interface PageLayout {
export type PageLayoutType = 'RECORD_INDEX' | 'RECORD_PAGE' | 'DASHBOARD' | 'STANDALONE_PAGE'
export interface ApplicationConnectionProviderOAuthConfig {
scopes: Scalars['String'][]
isClientCredentialsConfigured: Scalars['Boolean']
__typename: 'ApplicationConnectionProviderOAuthConfig'
}
export interface ApplicationConnectionProvider {
id: Scalars['UUID']
applicationId: Scalars['String']
type: Scalars['String']
name: Scalars['String']
displayName: Scalars['String']
oauth?: ApplicationConnectionProviderOAuthConfig
__typename: 'ApplicationConnectionProvider'
}
export interface Analytics {
/** Boolean that confirms query was dispatched */
success: Scalars['Boolean']
@@ -1940,7 +1956,6 @@ export interface MarketplaceApp {
id: Scalars['String']
name: Scalars['String']
description: Scalars['String']
icon: Scalars['String']
author: Scalars['String']
category: Scalars['String']
logo?: Scalars['String']
@@ -2223,6 +2238,10 @@ export interface ConnectedAccountDTO {
connectionParameters?: ImapSmtpCaldavConnectionParameters
lastSignedInAt?: Scalars['DateTime']
userWorkspaceId: Scalars['UUID']
applicationConnectionProviderId?: Scalars['UUID']
applicationId?: Scalars['UUID']
name?: Scalars['String']
visibility: Scalars['String']
createdAt: Scalars['DateTime']
updatedAt: Scalars['DateTime']
__typename: 'ConnectedAccountDTO'
@@ -2253,6 +2272,10 @@ export interface ConnectedAccountPublicDTO {
scopes?: Scalars['String'][]
lastSignedInAt?: Scalars['DateTime']
userWorkspaceId: Scalars['UUID']
applicationConnectionProviderId?: Scalars['UUID']
applicationId?: Scalars['UUID']
name?: Scalars['String']
visibility: Scalars['String']
createdAt: Scalars['DateTime']
updatedAt: Scalars['DateTime']
connectionParameters?: PublicImapSmtpCaldavConnectionParameters
@@ -2304,20 +2327,6 @@ export interface Skill {
__typename: 'Skill'
}
export interface AgentChatThread {
id: Scalars['UUID']
title?: Scalars['String']
totalInputTokens: Scalars['Int']
totalOutputTokens: Scalars['Int']
contextWindowTokens?: Scalars['Int']
conversationSize: Scalars['Int']
totalInputCredits: Scalars['Float']
totalOutputCredits: Scalars['Float']
createdAt: Scalars['DateTime']
updatedAt: Scalars['DateTime']
__typename: 'AgentChatThread'
}
export interface AgentMessage {
id: Scalars['UUID']
threadId: Scalars['UUID']
@@ -2331,6 +2340,22 @@ export interface AgentMessage {
__typename: 'AgentMessage'
}
export interface AgentChatThread {
id: Scalars['ID']
title?: Scalars['String']
totalInputTokens: Scalars['Int']
totalOutputTokens: Scalars['Int']
contextWindowTokens?: Scalars['Int']
conversationSize: Scalars['Int']
totalInputCredits: Scalars['Float']
totalOutputCredits: Scalars['Float']
createdAt: Scalars['DateTime']
updatedAt: Scalars['DateTime']
deletedAt?: Scalars['DateTime']
lastMessageAt?: Scalars['DateTime']
__typename: 'AgentChatThread'
}
export interface AiSystemPromptSection {
title: Scalars['String']
content: Scalars['String']
@@ -2363,22 +2388,6 @@ export interface AgentChatEvent {
__typename: 'AgentChatEvent'
}
export interface AgentChatThreadEdge {
/** The node containing the AgentChatThread */
node: AgentChatThread
/** Cursor for this node. */
cursor: Scalars['ConnectionCursor']
__typename: 'AgentChatThreadEdge'
}
export interface AgentChatThreadConnection {
/** Paging information */
pageInfo: PageInfo
/** Array of edges. */
edges: AgentChatThreadEdge[]
__typename: 'AgentChatThreadConnection'
}
export interface AgentTurnEvaluation {
id: Scalars['UUID']
turnId: Scalars['UUID']
@@ -2558,6 +2567,7 @@ export interface Query {
getPageLayoutTab: PageLayoutTab
getPageLayouts: PageLayout[]
getPageLayout?: PageLayout
applicationConnectionProviders: ApplicationConnectionProvider[]
getPageLayoutWidgets: PageLayoutWidget[]
getPageLayoutWidget: PageLayoutWidget
findOneLogicFunction: LogicFunction
@@ -2591,13 +2601,13 @@ export interface Query {
webhooks: Webhook[]
webhook?: Webhook
minimalMetadata: MinimalMetadata
chatThreads: AgentChatThread[]
chatThread: AgentChatThread
chatMessages: AgentMessage[]
chatStreamCatchupChunks: ChatStreamCatchupChunks
getAiSystemPromptPreview: AiSystemPromptPreview
skills: Skill[]
skill?: Skill
chatThreads: AgentChatThreadConnection
agentTurns: AgentTurn[]
checkUserExists: CheckUserExist
checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValid
@@ -2633,16 +2643,6 @@ export interface Query {
__typename: 'Query'
}
export type AgentChatThreadSortFields = 'id' | 'updatedAt'
/** Sort Directions */
export type SortDirection = 'ASC' | 'DESC'
/** Sort Nulls Options */
export type SortNulls = 'NULLS_FIRST' | 'NULLS_LAST'
export type EventLogTable = 'WORKSPACE_EVENT' | 'PAGEVIEW' | 'OBJECT_EVENT' | 'USAGE_EVENT' | 'APPLICATION_LOG'
export type UsageOperationType = 'AI_CHAT_TOKEN' | 'AI_WORKFLOW_TOKEN' | 'WORKFLOW_EXECUTION' | 'CODE_EXECUTION' | 'WEB_SEARCH'
@@ -2675,6 +2675,7 @@ export interface Mutation {
updateView: View
deleteView: Scalars['Boolean']
destroyView: Scalars['Boolean']
upsertViewWidget: View
createViewSort: ViewSort
updateViewSort: ViewSort
deleteViewSort: Scalars['Boolean']
@@ -2724,6 +2725,7 @@ export interface Mutation {
resetPageLayoutToDefault: PageLayout
resetPageLayoutWidgetToDefault: PageLayoutWidget
resetPageLayoutTabToDefault: PageLayoutTab
updateOneApplicationVariable: Scalars['Boolean']
createPageLayoutWidget: PageLayoutWidget
updatePageLayoutWidget: PageLayoutWidget
destroyPageLayoutWidget: Scalars['Boolean']
@@ -2773,6 +2775,10 @@ export interface Mutation {
createChatThread: AgentChatThread
sendChatMessage: SendChatMessageResult
stopAgentChatStream: Scalars['Boolean']
renameChatThread: AgentChatThread
archiveChatThread: AgentChatThread
unarchiveChatThread: AgentChatThread
deleteChatThread: Scalars['Boolean']
deleteQueuedChatMessage: Scalars['Boolean']
createSkill: Skill
updateSkill: Skill
@@ -2822,7 +2828,6 @@ export interface Mutation {
installApplication: Scalars['Boolean']
runWorkspaceMigration: Scalars['Boolean']
uninstallApplication: Scalars['Boolean']
updateOneApplicationVariable: Scalars['Boolean']
createOIDCIdentityProvider: SetupSso
createSAMLIdentityProvider: SetupSso
deleteSSOIdentityProvider: DeleteSso
@@ -3313,6 +3318,7 @@ export interface ApplicationGenqlSelection{
id?: boolean | number
name?: boolean | number
description?: boolean | number
logo?: boolean | number
version?: boolean | number
universalIdentifier?: boolean | number
packageJsonChecksum?: boolean | number
@@ -3536,7 +3542,6 @@ export interface UserGenqlSelection{
firstName?: boolean | number
lastName?: boolean | number
email?: boolean | number
defaultAvatarUrl?: boolean | number
isEmailVerified?: boolean | number
disabled?: boolean | number
canImpersonate?: boolean | number
@@ -3934,6 +3939,24 @@ export interface PageLayoutGenqlSelection{
__scalar?: boolean | number
}
export interface ApplicationConnectionProviderOAuthConfigGenqlSelection{
scopes?: boolean | number
isClientCredentialsConfigured?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface ApplicationConnectionProviderGenqlSelection{
id?: boolean | number
applicationId?: boolean | number
type?: boolean | number
name?: boolean | number
displayName?: boolean | number
oauth?: ApplicationConnectionProviderOAuthConfigGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
export interface AnalyticsGenqlSelection{
/** Boolean that confirms query was dispatched */
success?: boolean | number
@@ -4922,7 +4945,6 @@ export interface MarketplaceAppGenqlSelection{
id?: boolean | number
name?: boolean | number
description?: boolean | number
icon?: boolean | number
author?: boolean | number
category?: boolean | number
logo?: boolean | number
@@ -5228,6 +5250,10 @@ export interface ConnectedAccountDTOGenqlSelection{
connectionParameters?: ImapSmtpCaldavConnectionParametersGenqlSelection
lastSignedInAt?: boolean | number
userWorkspaceId?: boolean | number
applicationConnectionProviderId?: boolean | number
applicationId?: boolean | number
name?: boolean | number
visibility?: boolean | number
createdAt?: boolean | number
updatedAt?: boolean | number
__typename?: boolean | number
@@ -5261,6 +5287,10 @@ export interface ConnectedAccountPublicDTOGenqlSelection{
scopes?: boolean | number
lastSignedInAt?: boolean | number
userWorkspaceId?: boolean | number
applicationConnectionProviderId?: boolean | number
applicationId?: boolean | number
name?: boolean | number
visibility?: boolean | number
createdAt?: boolean | number
updatedAt?: boolean | number
connectionParameters?: PublicImapSmtpCaldavConnectionParametersGenqlSelection
@@ -5318,6 +5348,20 @@ export interface SkillGenqlSelection{
__scalar?: boolean | number
}
export interface AgentMessageGenqlSelection{
id?: boolean | number
threadId?: boolean | number
turnId?: boolean | number
agentId?: boolean | number
role?: boolean | number
status?: boolean | number
parts?: AgentMessagePartGenqlSelection
processedAt?: boolean | number
createdAt?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface AgentChatThreadGenqlSelection{
id?: boolean | number
title?: boolean | number
@@ -5329,20 +5373,8 @@ export interface AgentChatThreadGenqlSelection{
totalOutputCredits?: boolean | number
createdAt?: boolean | number
updatedAt?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface AgentMessageGenqlSelection{
id?: boolean | number
threadId?: boolean | number
turnId?: boolean | number
agentId?: boolean | number
role?: boolean | number
status?: boolean | number
parts?: AgentMessagePartGenqlSelection
processedAt?: boolean | number
createdAt?: boolean | number
deletedAt?: boolean | number
lastMessageAt?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
@@ -5384,24 +5416,6 @@ export interface AgentChatEventGenqlSelection{
__scalar?: boolean | number
}
export interface AgentChatThreadEdgeGenqlSelection{
/** The node containing the AgentChatThread */
node?: AgentChatThreadGenqlSelection
/** Cursor for this node. */
cursor?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface AgentChatThreadConnectionGenqlSelection{
/** Paging information */
pageInfo?: PageInfoGenqlSelection
/** Array of edges. */
edges?: AgentChatThreadEdgeGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
export interface AgentTurnEvaluationGenqlSelection{
id?: boolean | number
turnId?: boolean | number
@@ -5565,6 +5579,7 @@ export interface QueryGenqlSelection{
getPageLayoutTab?: (PageLayoutTabGenqlSelection & { __args: {id: Scalars['String']} })
getPageLayouts?: (PageLayoutGenqlSelection & { __args?: {objectMetadataId?: (Scalars['String'] | null), pageLayoutType?: (PageLayoutType | null)} })
getPageLayout?: (PageLayoutGenqlSelection & { __args: {id: Scalars['String']} })
applicationConnectionProviders?: (ApplicationConnectionProviderGenqlSelection & { __args: {applicationId: Scalars['UUID']} })
getPageLayoutWidgets?: (PageLayoutWidgetGenqlSelection & { __args: {pageLayoutTabId: Scalars['String']} })
getPageLayoutWidget?: (PageLayoutWidgetGenqlSelection & { __args: {id: Scalars['String']} })
findOneLogicFunction?: (LogicFunctionGenqlSelection & { __args: {input: LogicFunctionIdInput} })
@@ -5616,19 +5631,13 @@ export interface QueryGenqlSelection{
webhooks?: WebhookGenqlSelection
webhook?: (WebhookGenqlSelection & { __args: {id: Scalars['UUID']} })
minimalMetadata?: MinimalMetadataGenqlSelection
chatThreads?: AgentChatThreadGenqlSelection
chatThread?: (AgentChatThreadGenqlSelection & { __args: {id: Scalars['UUID']} })
chatMessages?: (AgentMessageGenqlSelection & { __args: {threadId: Scalars['UUID']} })
chatStreamCatchupChunks?: (ChatStreamCatchupChunksGenqlSelection & { __args: {threadId: Scalars['UUID']} })
getAiSystemPromptPreview?: AiSystemPromptPreviewGenqlSelection
skills?: SkillGenqlSelection
skill?: (SkillGenqlSelection & { __args: {id: Scalars['UUID']} })
chatThreads?: (AgentChatThreadConnectionGenqlSelection & { __args: {
/** Limit or page results. */
paging: CursorPaging,
/** Specify to filter the records returned. */
filter: AgentChatThreadFilter,
/** Specify to sort results. */
sorting: AgentChatThreadSort[]} })
agentTurns?: (AgentTurnGenqlSelection & { __args: {agentId: Scalars['UUID']} })
checkUserExists?: (CheckUserExistGenqlSelection & { __args: {email: Scalars['String'], captchaToken?: (Scalars['String'] | null)} })
checkWorkspaceInviteHashIsValid?: (WorkspaceInviteHashValidGenqlSelection & { __args: {inviteHash: Scalars['String']} })
@@ -5675,14 +5684,6 @@ export interface AgentIdInput {
/** The id of the agent. */
id: Scalars['UUID']}
export interface AgentChatThreadFilter {and?: (AgentChatThreadFilter[] | null),or?: (AgentChatThreadFilter[] | null),id?: (UUIDFilterComparison | null),updatedAt?: (DateFieldComparison | null)}
export interface DateFieldComparison {is?: (Scalars['Boolean'] | null),isNot?: (Scalars['Boolean'] | null),eq?: (Scalars['DateTime'] | null),neq?: (Scalars['DateTime'] | null),gt?: (Scalars['DateTime'] | null),gte?: (Scalars['DateTime'] | null),lt?: (Scalars['DateTime'] | null),lte?: (Scalars['DateTime'] | null),in?: (Scalars['DateTime'][] | null),notIn?: (Scalars['DateTime'][] | null),between?: (DateFieldComparisonBetween | null),notBetween?: (DateFieldComparisonBetween | null)}
export interface DateFieldComparisonBetween {lower: Scalars['DateTime'],upper: Scalars['DateTime']}
export interface AgentChatThreadSort {field: AgentChatThreadSortFields,direction: SortDirection,nulls?: (SortNulls | null)}
export interface EventLogQueryInput {table: EventLogTable,filters?: (EventLogFiltersInput | null),first?: (Scalars['Int'] | null),after?: (Scalars['String'] | null)}
export interface EventLogFiltersInput {eventType?: (Scalars['String'] | null),userWorkspaceId?: (Scalars['String'] | null),dateRange?: (EventLogDateRangeInput | null),recordId?: (Scalars['String'] | null),objectMetadataId?: (Scalars['String'] | null)}
@@ -5725,6 +5726,7 @@ export interface MutationGenqlSelection{
updateView?: (ViewGenqlSelection & { __args: {id: Scalars['String'], input: UpdateViewInput} })
deleteView?: { __args: {id: Scalars['String']} }
destroyView?: { __args: {id: Scalars['String']} }
upsertViewWidget?: (ViewGenqlSelection & { __args: {input: UpsertViewWidgetInput} })
createViewSort?: (ViewSortGenqlSelection & { __args: {input: CreateViewSortInput} })
updateViewSort?: (ViewSortGenqlSelection & { __args: {input: UpdateViewSortInput} })
deleteViewSort?: { __args: {input: DeleteViewSortInput} }
@@ -5774,6 +5776,7 @@ export interface MutationGenqlSelection{
resetPageLayoutToDefault?: (PageLayoutGenqlSelection & { __args: {id: Scalars['String']} })
resetPageLayoutWidgetToDefault?: (PageLayoutWidgetGenqlSelection & { __args: {id: Scalars['String']} })
resetPageLayoutTabToDefault?: (PageLayoutTabGenqlSelection & { __args: {id: Scalars['String']} })
updateOneApplicationVariable?: { __args: {key: Scalars['String'], value: Scalars['String'], applicationId: Scalars['UUID']} }
createPageLayoutWidget?: (PageLayoutWidgetGenqlSelection & { __args: {input: CreatePageLayoutWidgetInput} })
updatePageLayoutWidget?: (PageLayoutWidgetGenqlSelection & { __args: {id: Scalars['String'], input: UpdatePageLayoutWidgetInput} })
destroyPageLayoutWidget?: { __args: {id: Scalars['String']} }
@@ -5823,6 +5826,10 @@ export interface MutationGenqlSelection{
createChatThread?: AgentChatThreadGenqlSelection
sendChatMessage?: (SendChatMessageResultGenqlSelection & { __args: {threadId: Scalars['UUID'], text: Scalars['String'], messageId: Scalars['UUID'], browsingContext?: (Scalars['JSON'] | null), modelId?: (Scalars['String'] | null), fileIds?: (Scalars['UUID'][] | null)} })
stopAgentChatStream?: { __args: {threadId: Scalars['UUID']} }
renameChatThread?: (AgentChatThreadGenqlSelection & { __args: {id: Scalars['UUID'], title: Scalars['String']} })
archiveChatThread?: (AgentChatThreadGenqlSelection & { __args: {id: Scalars['UUID']} })
unarchiveChatThread?: (AgentChatThreadGenqlSelection & { __args: {id: Scalars['UUID']} })
deleteChatThread?: { __args: {id: Scalars['UUID']} }
deleteQueuedChatMessage?: { __args: {messageId: Scalars['UUID']} }
createSkill?: (SkillGenqlSelection & { __args: {input: CreateSkillInput} })
updateSkill?: (SkillGenqlSelection & { __args: {input: UpdateSkillInput} })
@@ -5872,7 +5879,6 @@ export interface MutationGenqlSelection{
installApplication?: { __args: {appRegistrationId: Scalars['String'], version?: (Scalars['String'] | null)} }
runWorkspaceMigration?: { __args: {workspaceMigration: WorkspaceMigrationInput} }
uninstallApplication?: { __args: {universalIdentifier: Scalars['String']} }
updateOneApplicationVariable?: { __args: {key: Scalars['String'], value: Scalars['String'], applicationId: Scalars['UUID']} }
createOIDCIdentityProvider?: (SetupSsoGenqlSelection & { __args: {input: SetupOIDCSsoInput} })
createSAMLIdentityProvider?: (SetupSsoGenqlSelection & { __args: {input: SetupSAMLSsoInput} })
deleteSSOIdentityProvider?: (DeleteSsoGenqlSelection & { __args: {input: DeleteSsoInput} })
@@ -5944,6 +5950,30 @@ export interface CreateViewInput {id?: (Scalars['UUID'] | null),name: Scalars['S
export interface UpdateViewInput {id?: (Scalars['UUID'] | null),name?: (Scalars['String'] | null),type?: (ViewType | null),icon?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isCompact?: (Scalars['Boolean'] | null),openRecordIn?: (ViewOpenRecordIn | null),kanbanAggregateOperation?: (AggregateOperations | null),kanbanAggregateOperationFieldMetadataId?: (Scalars['UUID'] | null),anyFieldFilterValue?: (Scalars['String'] | null),calendarLayout?: (ViewCalendarLayout | null),calendarFieldMetadataId?: (Scalars['UUID'] | null),visibility?: (ViewVisibility | null),mainGroupByFieldMetadataId?: (Scalars['UUID'] | null),shouldHideEmptyGroups?: (Scalars['Boolean'] | null)}
export interface UpsertViewWidgetInput {
/** The id of the view widget (page layout widget). */
widgetId: Scalars['UUID'],
/** The view fields to upsert. */
viewFields?: (UpsertViewWidgetViewFieldInput[] | null),
/** The view filters to upsert. */
viewFilters?: (UpsertViewWidgetViewFilterInput[] | null),
/** The view filter groups to upsert. */
viewFilterGroups?: (UpsertViewWidgetViewFilterGroupInput[] | null),
/** The view sorts to upsert. */
viewSorts?: (UpsertViewWidgetViewSortInput[] | null)}
export interface UpsertViewWidgetViewFieldInput {
/** The id of an existing view field to update. */
viewFieldId?: (Scalars['UUID'] | null),
/** The field metadata id. Used to create a new view field when viewFieldId is not provided. */
fieldMetadataId?: (Scalars['UUID'] | null),isVisible: Scalars['Boolean'],position: Scalars['Float'],size?: (Scalars['Float'] | null)}
export interface UpsertViewWidgetViewFilterInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],operand?: (ViewFilterOperand | null),value: Scalars['JSON'],viewFilterGroupId?: (Scalars['UUID'] | null),positionInViewFilterGroup?: (Scalars['Float'] | null),subFieldName?: (Scalars['String'] | null)}
export interface UpsertViewWidgetViewFilterGroupInput {id?: (Scalars['UUID'] | null),parentViewFilterGroupId?: (Scalars['UUID'] | null),logicalOperator?: (ViewFilterGroupLogicalOperator | null),positionInViewFilterGroup?: (Scalars['Float'] | null)}
export interface UpsertViewWidgetViewSortInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],direction?: (ViewSortDirection | null)}
export interface CreateViewSortInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],direction?: (ViewSortDirection | null),viewId: Scalars['UUID']}
export interface UpdateViewSortInput {
@@ -6827,6 +6857,22 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const ApplicationConnectionProviderOAuthConfig_possibleTypes: string[] = ['ApplicationConnectionProviderOAuthConfig']
export const isApplicationConnectionProviderOAuthConfig = (obj?: { __typename?: any } | null): obj is ApplicationConnectionProviderOAuthConfig => {
if (!obj?.__typename) throw new Error('__typename is missing in "isApplicationConnectionProviderOAuthConfig"')
return ApplicationConnectionProviderOAuthConfig_possibleTypes.includes(obj.__typename)
}
const ApplicationConnectionProvider_possibleTypes: string[] = ['ApplicationConnectionProvider']
export const isApplicationConnectionProvider = (obj?: { __typename?: any } | null): obj is ApplicationConnectionProvider => {
if (!obj?.__typename) throw new Error('__typename is missing in "isApplicationConnectionProvider"')
return ApplicationConnectionProvider_possibleTypes.includes(obj.__typename)
}
const Analytics_possibleTypes: string[] = ['Analytics']
export const isAnalytics = (obj?: { __typename?: any } | null): obj is Analytics => {
if (!obj?.__typename) throw new Error('__typename is missing in "isAnalytics"')
@@ -8019,14 +8065,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const AgentChatThread_possibleTypes: string[] = ['AgentChatThread']
export const isAgentChatThread = (obj?: { __typename?: any } | null): obj is AgentChatThread => {
if (!obj?.__typename) throw new Error('__typename is missing in "isAgentChatThread"')
return AgentChatThread_possibleTypes.includes(obj.__typename)
}
const AgentMessage_possibleTypes: string[] = ['AgentMessage']
export const isAgentMessage = (obj?: { __typename?: any } | null): obj is AgentMessage => {
if (!obj?.__typename) throw new Error('__typename is missing in "isAgentMessage"')
@@ -8035,6 +8073,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const AgentChatThread_possibleTypes: string[] = ['AgentChatThread']
export const isAgentChatThread = (obj?: { __typename?: any } | null): obj is AgentChatThread => {
if (!obj?.__typename) throw new Error('__typename is missing in "isAgentChatThread"')
return AgentChatThread_possibleTypes.includes(obj.__typename)
}
const AiSystemPromptSection_possibleTypes: string[] = ['AiSystemPromptSection']
export const isAiSystemPromptSection = (obj?: { __typename?: any } | null): obj is AiSystemPromptSection => {
if (!obj?.__typename) throw new Error('__typename is missing in "isAiSystemPromptSection"')
@@ -8075,22 +8121,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const AgentChatThreadEdge_possibleTypes: string[] = ['AgentChatThreadEdge']
export const isAgentChatThreadEdge = (obj?: { __typename?: any } | null): obj is AgentChatThreadEdge => {
if (!obj?.__typename) throw new Error('__typename is missing in "isAgentChatThreadEdge"')
return AgentChatThreadEdge_possibleTypes.includes(obj.__typename)
}
const AgentChatThreadConnection_possibleTypes: string[] = ['AgentChatThreadConnection']
export const isAgentChatThreadConnection = (obj?: { __typename?: any } | null): obj is AgentChatThreadConnection => {
if (!obj?.__typename) throw new Error('__typename is missing in "isAgentChatThreadConnection"')
return AgentChatThreadConnection_possibleTypes.includes(obj.__typename)
}
const AgentTurnEvaluation_possibleTypes: string[] = ['AgentTurnEvaluation']
export const isAgentTurnEvaluation = (obj?: { __typename?: any } | null): obj is AgentTurnEvaluation => {
if (!obj?.__typename) throw new Error('__typename is missing in "isAgentTurnEvaluation"')
@@ -8822,21 +8852,6 @@ export const enumAllMetadataName = {
webhook: 'webhook' as const
}
export const enumAgentChatThreadSortFields = {
id: 'id' as const,
updatedAt: 'updatedAt' as const
}
export const enumSortDirection = {
ASC: 'ASC' as const,
DESC: 'DESC' as const
}
export const enumSortNulls = {
NULLS_FIRST: 'NULLS_FIRST' as const,
NULLS_LAST: 'NULLS_LAST' as const
}
export const enumEventLogTable = {
WORKSPACE_EVENT: 'WORKSPACE_EVENT' as const,
PAGEVIEW: 'PAGEVIEW' as const,
File diff suppressed because it is too large Load Diff
@@ -4,6 +4,6 @@ set -e
echo "==> START Registering cron jobs"
cd /app/packages/twenty-server
yarn command:prod cron:register:all --dev-mode
yarn command:prod cron:register:all
echo "==> DONE"
@@ -11,10 +11,12 @@ COPY ./nx.json .
COPY ./.yarn/releases /app/.yarn/releases
COPY ./.yarn/patches /app/.yarn/patches
COPY ./packages/twenty-oxlint-rules /app/packages/twenty-oxlint-rules
COPY ./packages/twenty-shared/package.json /app/packages/twenty-shared/package.json
COPY ./packages/twenty-website-new/package.json /app/packages/twenty-website-new/package.json
RUN yarn
COPY ./packages/twenty-shared /app/packages/twenty-shared
COPY ./packages/twenty-website-new /app/packages/twenty-website-new
RUN npx nx build twenty-website-new
+24 -18
View File
@@ -1,8 +1,26 @@
# ===========================================================================
# Shared build stages (used by both targets)
# Dependency stages
# ===========================================================================
FROM node:24-alpine AS common-deps
FROM node:24-alpine AS front-deps
WORKDIR /app
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
COPY ./packages/twenty-ui/package.json /app/packages/twenty-ui/
COPY ./packages/twenty-shared/package.json /app/packages/twenty-shared/
COPY ./packages/twenty-front/package.json /app/packages/twenty-front/
COPY ./packages/twenty-front-component-renderer/package.json /app/packages/twenty-front-component-renderer/
COPY ./packages/twenty-sdk/package.json /app/packages/twenty-sdk/
COPY ./packages/twenty-client-sdk/package.json /app/packages/twenty-client-sdk/
RUN yarn workspaces focus twenty twenty-front twenty-front-component-renderer twenty-ui twenty-shared twenty-sdk twenty-client-sdk && yarn cache clean && npx nx reset
FROM node:24-alpine AS server-deps
WORKDIR /app
@@ -13,22 +31,16 @@ COPY ./.yarn/patches /app/.yarn/patches
COPY ./packages/twenty-emails/package.json /app/packages/twenty-emails/
COPY ./packages/twenty-server/package.json /app/packages/twenty-server/
COPY ./packages/twenty-server/patches /app/packages/twenty-server/patches
COPY ./packages/twenty-ui/package.json /app/packages/twenty-ui/
COPY ./packages/twenty-shared/package.json /app/packages/twenty-shared/
COPY ./packages/twenty-front/package.json /app/packages/twenty-front/
COPY ./packages/twenty-front-component-renderer/package.json /app/packages/twenty-front-component-renderer/
COPY ./packages/twenty-sdk/package.json /app/packages/twenty-sdk/
COPY ./packages/twenty-client-sdk/package.json /app/packages/twenty-client-sdk/
RUN yarn && yarn cache clean && npx nx reset
RUN yarn workspaces focus twenty twenty-server twenty-emails twenty-shared twenty-client-sdk && yarn cache clean && npx nx reset
FROM common-deps AS twenty-server-build
FROM server-deps AS twenty-server-build
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-client-sdk /app/packages/twenty-client-sdk
COPY ./packages/twenty-server /app/packages/twenty-server
@@ -44,10 +56,10 @@ RUN npx nx run twenty-server:build
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-client-sdk twenty-server
RUN yarn workspaces focus --production twenty-emails twenty-shared twenty-client-sdk twenty-server
FROM common-deps AS twenty-front-build
FROM front-deps AS twenty-front-build
COPY ./packages/twenty-front /app/packages/twenty-front
COPY ./packages/twenty-front-component-renderer /app/packages/twenty-front-component-renderer
@@ -99,11 +111,8 @@ COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-shared/package
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-client-sdk/package.json /app/packages/twenty-client-sdk/
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-client-sdk/dist /app/packages/twenty-client-sdk/dist
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/
LABEL org.opencontainers.image.source=https://github.com/twentyhq/twenty
LABEL org.opencontainers.image.description="Twenty server image (no frontend)."
@@ -208,11 +217,8 @@ COPY --from=twenty-server-build /app/packages/twenty-shared/package.json /app/pa
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-client-sdk/package.json /app/packages/twenty-client-sdk/
COPY --from=twenty-server-build /app/packages/twenty-client-sdk/dist /app/packages/twenty-client-sdk/dist
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
@@ -0,0 +1,193 @@
---
title: Connections
description: Let your app act on a user's behalf in third-party services via OAuth.
icon: 'plug'
---
Connections are credentials a user holds for an external service (Linear, GitHub, Slack, ...). Your app declares **how** those credentials are obtained — a **connection provider** — and consumes them at runtime to make authenticated calls to the third-party API.
Today only OAuth 2.0 is supported. Future credential types (personal access tokens, API keys, basic auth) will plug into the same surface — apps already using `defineConnectionProvider({ type: 'oauth', ... })` won't need to migrate.
<AccordionGroup>
<Accordion title="defineConnectionProvider" description="Declare how your app's connections are obtained">
A connection provider describes the OAuth handshake your app needs. The user clicks "Add connection" in your app's settings, completes the provider's consent screen, and a `ConnectedAccount` row is created in their workspace.
A working setup needs **two files** — the connection provider, and a matching `serverVariables` declaration on `defineApplication` that holds the OAuth client credentials.
```ts src/connection-providers/linear-connection.ts
import { defineConnectionProvider } from 'twenty-sdk/define';
export default defineConnectionProvider({
universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f',
name: 'linear',
displayName: 'Linear',
icon: 'IconBrandLinear',
type: 'oauth',
oauth: {
authorizationEndpoint: 'https://linear.app/oauth/authorize',
tokenEndpoint: 'https://api.linear.app/oauth/token',
scopes: ['read', 'write'],
// These must match keys in `defineApplication.serverVariables` below.
clientIdVariable: 'LINEAR_CLIENT_ID',
clientSecretVariable: 'LINEAR_CLIENT_SECRET',
// Optional: defaults to 'json'. Some providers (Linear, Slack) want
// 'form-urlencoded' for the token request.
tokenRequestContentType: 'form-urlencoded',
// Optional: defaults to true. Disable only if the provider rejects PKCE.
usePkce: false,
// Optional: extra query params on the authorize URL.
// authorizationParams: { prompt: 'consent' },
// Optional: provider's RFC 7009 token revocation endpoint, called on disconnect.
// revokeEndpoint: 'https://example.com/oauth/revoke',
},
});
```
```ts src/application.config.ts
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: '...',
displayName: 'Linear',
description: 'Connect Linear to Twenty.',
defaultRoleUniversalIdentifier: '...',
// OAuth client credentials live on the app registration (one OAuth app per
// Twenty server, configured by the admin) — not per-workspace. Declare them
// as serverVariables so the admin can fill them in once for all installs.
serverVariables: {
LINEAR_CLIENT_ID: {
description: 'OAuth client ID from your Linear OAuth application.',
isSecret: false,
isRequired: true,
},
LINEAR_CLIENT_SECRET: {
description: 'OAuth client secret from your Linear OAuth application.',
isSecret: true,
isRequired: true,
},
},
});
```
Key points:
- `name` is the unique identifier string used in `listConnections({ providerName })` (kebab-case, must match `^[a-z][a-z0-9-]*$`).
- `displayName` shows in the per-app settings tab and in the AI tool list.
- `clientIdVariable` / `clientSecretVariable` are **names**, not values — they must match keys declared in `defineApplication.serverVariables`. The actual `client_id` and `client_secret` are entered by the server admin through the app registration UI, never committed to your repo.
- Use `serverVariables` (not `applicationVariables`) — OAuth credentials are server-wide and one OAuth app per Twenty server.
- Until both `serverVariables` are filled in, the per-app settings tab shows a "needs server admin" hint and the "Add connection" button is disabled.
- `type: 'oauth'` is the only supported value today. The discriminator is forward-compatible: future types (`'pat'`, `'api-key'`, ...) will add new sub-config blocks alongside `oauth`.
The OAuth callback URL your provider needs to whitelist is:
```
https://<your-twenty-server>/apps/oauth/callback
```
</Accordion>
<Accordion title="listConnections / getConnection" description="Use connections from a logic function">
Inside a logic function handler, `listConnections({ providerName })` returns this app's `ConnectedAccount` rows for the given provider, with refreshed access tokens.
```ts src/logic-functions/handlers/create-linear-issue-handler.ts
import { listConnections } from 'twenty-sdk/logic-function';
export const createLinearIssueHandler = async (input: {
teamId?: string;
title?: string;
}) => {
if (!input.teamId || !input.title) {
return { success: false, error: 'teamId and title are required' };
}
const connections = await listConnections({ providerName: 'linear' });
// Workspace-shared credentials win when present; fall back to the first
// user-visibility one. For HTTP-route triggers you typically pick the
// request user's connection via event.userWorkspaceId instead.
const connection =
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
if (!connection) {
return {
success: false,
error:
'Linear is not connected. Open the app settings and click "Add connection".',
};
}
// Use connection.accessToken to call the third-party API.
const response = await fetch('https://api.linear.app/graphql', {
method: 'POST',
headers: {
Authorization: `Bearer ${connection.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`,
}),
});
return { success: response.ok };
};
```
Each connection has:
| Field | Description |
| ----------------- | -------------------------------------------------------------------------------------------------------- |
| `id` | Unique row id; pass to `getConnection(id)` to refetch a single one |
| `visibility` | `'user'` (private to one workspace member) or `'workspace'` (shared with all members) |
| `scopes` | OAuth permissions granted by the upstream provider (distinct from `visibility` — those are unrelated) |
| `userWorkspaceId` | The owner's userWorkspace id — useful for picking "the request user's connection" in HTTP-route triggers |
| `accessToken` | Fresh OAuth access token (refreshed automatically if expired) |
| `name` / `handle` | The connection's display name (auto-derived at OAuth callback, user-renameable) |
| `authFailedAt` | Set when the most recent refresh failed; the user must reconnect |
Key points:
- Pass `{ providerName }` to filter by provider; omit it to get all connections this app owns across all providers.
- The server transparently refreshes the access token before returning. Your handler always sees a usable token (or `authFailedAt` set).
- `getConnection(id)` is the single-row equivalent.
</Accordion>
<Accordion title="Per-user vs workspace-shared visibility" description="How users choose between private and shared credentials">
When a user clicks "Add connection," they're prompted to pick a visibility:
- **Just for me** — the credential is private to the connecting user. Any logic function called on their behalf (HTTP-route trigger with `isAuthRequired: true`) sees it; cron triggers and database events do not.
- **Workspace shared** — any workspace member can use the credential. Cron / database triggers also see it, since they have no request user.
Use the right one for each handler:
```ts
// HTTP-route trigger — prefer the request user's own connection.
const conn =
connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ??
connections.find((c) => c.visibility === 'workspace');
// Cron trigger — no request user; only shared credentials are sensible.
const conn = connections.find((c) => c.visibility === 'workspace');
```
Multiple connections per (user, provider) are allowed, so the same user can hold "Personal Linear" and "Work Linear" side by side.
</Accordion>
<Accordion title="One-time provider setup" description="Register your OAuth app with the third-party service">
For each connection provider, the server admin needs to register an OAuth app at the third party first.
1. Go to the provider's developer settings (e.g. https://linear.app/settings/api/applications/new).
2. Set the **Redirect URI** to `<SERVER_URL>/apps/oauth/callback`.
3. Copy the generated **Client ID** and **Client Secret**.
4. Open the installed app in Twenty as a server admin → set the values on the corresponding `serverVariables`.
5. Workspace members can then add connections from the per-app **Connections** section.
</Accordion>
</AccordionGroup>
@@ -77,7 +77,6 @@ export default defineApplication({
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
displayName: 'My Twenty App',
description: 'My first Twenty app',
icon: 'IconWorld',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
@@ -88,11 +88,11 @@ yarn twenty dev --verbose
```
<Warning>
Dev mode is only available on Twenty instances running in development (`NODE_ENV=development`). Production instances reject dev sync requests. Use `yarn twenty deploy` to deploy to production servers — see [Publishing Apps](/developers/extend/apps/publishing) for details.
Dev mode is only available on Twenty instances running in development (`NODE_ENV=development`). Production instances reject dev sync requests. Use `yarn twenty deploy` followed by `yarn twenty install` to publish and install on production servers — `deploy` publishes to the application registry, while `install` installs it on a given workspace. See [Publishing Apps](/developers/extend/apps/publishing) for details.
</Warning>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="Dev mode terminal output" />
<img src="/images/docs/developers/extends/apps/dev.png" alt="Dev mode terminal output" />
</div>
#### One-shot sync with `yarn twenty dev --once`
@@ -127,13 +127,7 @@ Click on **My twenty app** to open its **application registration**. A registrat
Click **View installed app** to see the installed app. The **About** tab shows the current version and management options:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Installed app — About tab" />
</div>
Switch to the **Content** tab to see everything your app provides — objects, fields, logic functions, and agents:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-4.png" alt="Installed app — Content tab" />
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Installed app" />
</div>
You are all set! Edit any file in `src/` and the changes will be picked up automatically.
@@ -219,13 +213,31 @@ The scaffolder already started a local Twenty server for you. To manage it later
| `yarn twenty server start --port 3030` | Start on a custom port |
| `yarn twenty server start --test` | Start a separate test instance on port 2021 |
| `yarn twenty server stop` | Stop the server (preserves data) |
| `yarn twenty server status` | Show server status, URL, and credentials |
| `yarn twenty server status` | Show server status, URL, version, and credentials |
| `yarn twenty server logs` | Stream server logs |
| `yarn twenty server logs --lines 100` | Show the last 100 log lines |
| `yarn twenty server reset` | Delete all data and start fresh |
| `yarn twenty server upgrade` | Pull the latest `twenty-app-dev` image and recreate the container |
| `yarn twenty server upgrade 2.2.0` | Upgrade to a specific version |
Data is persisted across restarts in two Docker volumes (`twenty-app-dev-data` for PostgreSQL, `twenty-app-dev-storage` for files). Use `reset` to wipe everything and start fresh.
### Upgrading the server image
Use `yarn twenty server upgrade` to check for a newer `twenty-app-dev` Docker image and update the container. The command pulls the image, compares it against the one the container was created from, and only recreates the container if the image actually changed. Your data volumes are preserved — only the container is replaced.
```bash filename="Terminal"
# Upgrade to the latest version (skips recreation if already up to date)
yarn twenty server upgrade
# Upgrade to a specific version
yarn twenty server upgrade 2.2.0
```
If a newer image is available and the container was running, the upgrade command automatically starts a new container with the updated image. Run `yarn twenty server start` afterward to wait for it to become healthy. If the image hasn't changed, the container is left untouched.
You can verify the running version with `yarn twenty server status`, which displays the `APP_VERSION` from the container.
### Running a test instance
Pass `--test` to any `server` command to manage a second, fully isolated instance — useful for running integration tests or experimenting without touching your main dev data.
@@ -234,9 +246,10 @@ Pass `--test` to any `server` command to manage a second, fully isolated instanc
|---------|-------------|
| `yarn twenty server start --test` | Start the test instance (defaults to port 2021) |
| `yarn twenty server stop --test` | Stop the test instance |
| `yarn twenty server status --test` | Show test instance status, URL, and credentials |
| `yarn twenty server status --test` | Show test instance status, URL, version, and credentials |
| `yarn twenty server logs --test` | Stream test instance logs |
| `yarn twenty server reset --test` | Wipe test data and start fresh |
| `yarn twenty server upgrade --test` | Upgrade the test instance image |
The test instance runs in its own Docker container (`twenty-app-dev-test`) with dedicated volumes (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`) and config, so it can run in parallel with your main instance without conflicts. Combine `--test` with `--port` to override the default 2021.
@@ -77,6 +77,39 @@ Pre-release tags work as expected: bumping `1.0.0-rc.1` → `1.0.0-rc.2` is allo
{/* TODO: add screenshot of the Upgrade button */}
### Server version compatibility
If your app uses a feature introduced in a specific Twenty server version (for example, OAuth providers added in v2.3.0), you should declare the minimum server version your app requires using the `engines.twenty` field in `package.json`:
```json filename="package.json"
{
"name": "twenty-my-app",
"version": "1.0.0",
"engines": {
"node": "^24.5.0",
"twenty": ">=2.3.0"
}
}
```
The value is a standard [semver range](https://github.com/npm/node-semver#ranges). Common patterns:
| Range | Meaning |
|-------|---------|
| `>=2.3.0` | Any server from 2.3.0 onward |
| `>=2.3.0 <3.0.0` | 2.3.0 or later, but below the next major |
| `^2.3.0` | Same as `>=2.3.0 <3.0.0` |
**What happens at deploy and install time:**
- If `engines.twenty` is set and the target server's version does not satisfy the range, the deploy (tarball upload) or install is rejected with a `SERVER_VERSION_INCOMPATIBLE` error and a message indicating both the required range and the actual server version.
- If `engines.twenty` is **not set**, the app is accepted on any server version (backward-compatible with existing apps).
- If the server has no `APP_VERSION` configured, the check is skipped.
<Note>
The server is the authoritative check — it validates `engines.twenty` on both tarball upload and workspace install. If you deploy a tarball out-of-band or install from the marketplace, the server still enforces compatibility.
</Note>
## Automated CI/CD (scaffolded workflows)
Apps generated with `create-twenty-app` ship with two GitHub Actions workflows out of the box, under `.github/workflows/`. They are ready to run as soon as you push the repo to GitHub — no extra setup is needed for CI, and CD only requires a single secret.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 773 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 724 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 662 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

@@ -0,0 +1,193 @@
---
title: Verbindungen
description: Ermöglichen Sie Ihrer App, im Namen eines Benutzers über OAuth in Diensten von Drittanbietern zu handeln.
icon: plug
---
Verbindungen sind Anmeldedaten, die ein Benutzer für einen externen Dienst besitzt (Linear, GitHub, Slack, ...). Ihre App legt fest, **wie** diese Anmeldedaten bezogen werden — ein **Verbindungsanbieter** — und verwendet sie zur Laufzeit, um authentifizierte Aufrufe an die Drittanbieter-API zu tätigen.
Derzeit wird nur OAuth 2.0 unterstützt. Zukünftige Anmeldedatentypen (Personal Access Tokens, API-Schlüssel, Basic Auth) werden in dieselbe Oberfläche integriert — Apps, die bereits `defineConnectionProvider({ type: 'oauth', ... })` müssen nicht migriert werden.
<AccordionGroup>
<Accordion title="defineConnectionProvider" description="Legen Sie fest, wie die Verbindungen Ihrer App bezogen werden">
Ein Verbindungsanbieter beschreibt den OAuth-Handshake, den Ihre App benötigt. Der Benutzer klickt in den Einstellungen Ihrer App auf "Verbindung hinzufügen", schließt den Zustimmungsbildschirm des Anbieters ab, und in seinem Arbeitsbereich wird eine `ConnectedAccount`-Zeile erstellt.
Eine funktionierende Einrichtung benötigt **zwei Dateien** — den Verbindungsanbieter und eine passende `serverVariables`-Deklaration in `defineApplication`, die die OAuth-Client-Anmeldedaten enthält.
```ts src/connection-providers/linear-connection.ts
import { defineConnectionProvider } from 'twenty-sdk/define';
export default defineConnectionProvider({
universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f',
name: 'linear',
displayName: 'Linear',
icon: 'IconBrandLinear',
type: 'oauth',
oauth: {
authorizationEndpoint: 'https://linear.app/oauth/authorize',
tokenEndpoint: 'https://api.linear.app/oauth/token',
scopes: ['read', 'write'],
// These must match keys in `defineApplication.serverVariables` below.
clientIdVariable: 'LINEAR_CLIENT_ID',
clientSecretVariable: 'LINEAR_CLIENT_SECRET',
// Optional: defaults to 'json'. Some providers (Linear, Slack) want
// 'form-urlencoded' for the token request.
tokenRequestContentType: 'form-urlencoded',
// Optional: defaults to true. Disable only if the provider rejects PKCE.
usePkce: false,
// Optional: extra query params on the authorize URL.
// authorizationParams: { prompt: 'consent' },
// Optional: provider's RFC 7009 token revocation endpoint, called on disconnect.
// revokeEndpoint: 'https://example.com/oauth/revoke',
},
});
```
```ts src/application.config.ts
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: '...',
displayName: 'Linear',
description: 'Connect Linear to Twenty.',
defaultRoleUniversalIdentifier: '...',
// OAuth client credentials live on the app registration (one OAuth app per
// Twenty server, configured by the admin) — not per-workspace. Declare them
// as serverVariables so the admin can fill them in once for all installs.
serverVariables: {
LINEAR_CLIENT_ID: {
description: 'OAuth client ID from your Linear OAuth application.',
isSecret: false,
isRequired: true,
},
LINEAR_CLIENT_SECRET: {
description: 'OAuth client secret from your Linear OAuth application.',
isSecret: true,
isRequired: true,
},
},
});
```
Hauptpunkte:
* `name` ist die eindeutige Bezeichner-Zeichenfolge, die in `listConnections({ providerName })` verwendet wird (kebab-case, muss `^[a-z][a-z0-9-]*$` entsprechen).
* `displayName` wird im Einstellungs-Tab der jeweiligen App und in der KI-Toolliste angezeigt.
* `clientIdVariable` / `clientSecretVariable` sind **Namen**, keine Werte — sie müssen den in `defineApplication.serverVariables` deklarierten Schlüsseln entsprechen. Die tatsächlichen `client_id` und `client_secret` werden vom Serveradministrator über die App-Registrierungsoberfläche eingegeben und niemals in Ihr Repository eingecheckt.
* Verwenden Sie `serverVariables` (nicht `applicationVariables`) — OAuth-Anmeldedaten gelten serverweit und es gibt eine OAuth-App pro Twenty-Server.
* Solange beide `serverVariables` nicht ausgefüllt sind, zeigt der Einstellungs-Tab pro App den Hinweis "Benötigt Server-Admin" an und der Button "Verbindung hinzufügen" ist deaktiviert.
* `type: 'oauth'` ist derzeit der einzige unterstützte Wert. Der Diskriminator ist vorwärtskompatibel: zukünftige Typen (`'pat'`, `'api-key'`, ...) werden neue Unterkonfigurationsblöcke neben `oauth` hinzufügen.
Die OAuth-Callback-URL, die Ihr Anbieter auf die Whitelist setzen muss, lautet:
```
https://<your-twenty-server>/apps/oauth/callback
```
</Accordion>
<Accordion title="listConnections / getConnection" description="Verbindungen aus einer Logikfunktion verwenden">
Innerhalb eines Logikfunktions-Handlers gibt `listConnections({ providerName })` die `ConnectedAccount`-Zeilen dieser App für den angegebenen Anbieter zurück, mit aktualisierten Zugriffstoken.
```ts src/logic-functions/handlers/create-linear-issue-handler.ts
import { listConnections } from 'twenty-sdk/logic-function';
export const createLinearIssueHandler = async (input: {
teamId?: string;
title?: string;
}) => {
if (!input.teamId || !input.title) {
return { success: false, error: 'teamId and title are required' };
}
const connections = await listConnections({ providerName: 'linear' });
// Workspace-shared credentials win when present; fall back to the first
// user-visibility one. For HTTP-route triggers you typically pick the
// request user's connection via event.userWorkspaceId instead.
const connection =
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
if (!connection) {
return {
success: false,
error:
'Linear is not connected. Open the app settings and click "Add connection".',
};
}
// Use connection.accessToken to call the third-party API.
const response = await fetch('https://api.linear.app/graphql', {
method: 'POST',
headers: {
Authorization: `Bearer ${connection.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`,
}),
});
return { success: response.ok };
};
```
Jede Verbindung hat:
| Feld | Beschreibung |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
| `id` | Eindeutige Zeilen-ID; an `getConnection(id)` übergeben, um eine einzelne Verbindung erneut abzurufen |
| `sichtbarkeit` | `'user'` (privat für ein Mitglied des Arbeitsbereichs) oder `'workspace'` (mit allen Mitgliedern geteilt) |
| `geltungsbereiche` | Vom Upstream-Anbieter gewährte OAuth-Berechtigungen (unabhängig von `visibility` — diese sind nicht miteinander verknüpft) |
| `userWorkspaceId` | Die userWorkspace-ID des Eigentümers — nützlich, um "die Verbindung des anfragenden Benutzers" in HTTP-Routen-Triggern auszuwählen |
| `accessToken` | Frisches OAuth-Zugriffstoken (wird bei Ablauf automatisch erneuert) |
| `name` / `handle` | Anzeigename der Verbindung (automatisch beim OAuth-Callback abgeleitet, vom Benutzer umbenennbar) |
| `authFailedAt` | Gesetzt, wenn die jüngste Aktualisierung fehlgeschlagen ist; der Benutzer muss die Verbindung erneut herstellen |
Hauptpunkte:
* Übergeben Sie `{ providerName }`, um nach Anbieter zu filtern; lassen Sie es weg, um alle Verbindungen dieser App über alle Anbieter hinweg zu erhalten.
* Der Server aktualisiert das Zugriffstoken vor der Rückgabe transparent. Ihr Handler sieht stets ein verwendbares Token (oder `authFailedAt` ist gesetzt).
* `getConnection(id)` ist das Pendant für eine einzelne Zeile.
</Accordion>
<Accordion title="Sichtbarkeit: pro Benutzer vs. im Arbeitsbereich geteilt" description="Wie Benutzer zwischen privaten und geteilten Anmeldedaten wählen">
Wenn ein Benutzer auf "Verbindung hinzufügen" klickt, wird er aufgefordert, eine Sichtbarkeit auszuwählen:
* **Nur für mich** — die Anmeldedaten sind für den sich verbindenden Benutzer privat. Jede Logikfunktion, die in seinem/ihrem Auftrag aufgerufen wird (HTTP-Routen-Trigger mit `isAuthRequired: true`), sieht sie; Cron-Trigger und Datenbankereignisse nicht.
* **Im Arbeitsbereich geteilt** — jedes Arbeitsbereichsmitglied kann die Anmeldedaten verwenden. Cron-/Datenbank-Trigger sehen sie ebenfalls, da sie keinen anfragenden Benutzer haben.
Verwenden Sie für jeden Handler die richtige Option:
```ts
// HTTP-route trigger — prefer the request user's own connection.
const conn =
connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ??
connections.find((c) => c.visibility === 'workspace');
// Cron trigger — no request user; only shared credentials are sensible.
const conn = connections.find((c) => c.visibility === 'workspace');
```
Mehrere Verbindungen pro (Benutzer, Anbieter) sind erlaubt, sodass derselbe Benutzer "Persönliches Linear" und "Arbeits-Linear" nebeneinander haben kann.
</Accordion>
<Accordion title="Einmalige Anbietereinrichtung" description="Registrieren Sie Ihre OAuth-App beim Drittanbieterdienst">
Für jeden Verbindungsanbieter muss der Serveradministrator zunächst eine OAuth-App beim Drittanbieter registrieren.
1. Gehen Sie zu den Entwickler-Einstellungen des Anbieters (z. B. https://linear.app/settings/api/applications/new).
2. Setzen Sie die **Redirect-URI** auf `\<SERVER_URL>/apps/oauth/callback`.
3. Kopieren Sie die generierte **Client ID** und das **Client Secret**.
4. Öffnen Sie die installierte App in Twenty als Serveradministrator → setzen Sie die Werte in den entsprechenden `serverVariables`.
5. Mitglieder des Arbeitsbereichs können dann Verbindungen im **Verbindungen**-Abschnitt der jeweiligen App hinzufügen.
</Accordion>
</AccordionGroup>
@@ -77,7 +77,6 @@ export default defineApplication({
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
displayName: 'My Twenty App',
description: 'My first Twenty app',
icon: 'IconWorld',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
@@ -88,11 +88,11 @@ yarn twenty dev --verbose
```
<Warning>
Der Dev-Modus ist nur auf Twenty-Instanzen verfügbar, die im Entwicklungsmodus laufen (`NODE_ENV=development`). Produktionsinstanzen lehnen Dev-Synchronisierungsanfragen ab. Verwenden Sie `yarn twenty deploy`, um auf Produktionsservern bereitzustellen — Details finden Sie unter [Apps veröffentlichen](/l/de/developers/extend/apps/publishing).
Der Dev-Modus ist nur auf Twenty-Instanzen verfügbar, die im Entwicklungsmodus laufen (`NODE_ENV=development`). Produktionsinstanzen lehnen Dev-Synchronisierungsanfragen ab. Verwenden Sie `yarn twenty deploy`, gefolgt von `yarn twenty install`, um auf Produktionsservern zu veröffentlichen und zu installieren — `deploy` veröffentlicht im Anwendungsregister, während `install` es in einem angegebenen Arbeitsbereich installiert. Details finden Sie unter [Apps veröffentlichen](/l/de/developers/extend/apps/publishing).
</Warning>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="Terminalausgabe im Dev-Modus" />
<img src="/images/docs/developers/extends/apps/dev.png" alt="Terminalausgabe im Dev-Modus" />
</div>
#### Einmalige Synchronisierung mit `yarn twenty dev --once`
@@ -127,13 +127,7 @@ Klicken Sie auf **My twenty app**, um die **Anwendungsregistrierung** zu öffnen
Klicken Sie auf **View installed app**, um die installierte App anzuzeigen. Die Registerkarte **About** zeigt die aktuelle Version und Verwaltungsoptionen:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Installierte App — Registerkarte About" />
</div>
Wechseln Sie zur Registerkarte **Content**, um alles zu sehen, was Ihre App bereitstellt — Objekte, Felder, Logikfunktionen und Agenten:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-4.png" alt="Installierte App — Registerkarte Content" />
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Installierte App" />
</div>
Alles erledigt! Bearbeiten Sie eine beliebige Datei in `src/`, und die Änderungen werden automatisch übernommen.
@@ -213,30 +207,49 @@ Die Beispiele stammen aus dem Verzeichnis [twenty-apps/examples](https://github.
Der Scaffolder hat bereits einen lokalen Twenty-Server für Sie gestartet. Um ihn später zu verwalten, verwenden Sie `yarn twenty server`:
| Befehl | Beschreibung |
| -------------------------------------- | ----------------------------------------------------------- |
| `yarn twenty server start` | Lokalen Server starten (lädt das Image bei Bedarf herunter) |
| `yarn twenty server start --port 3030` | Auf einem benutzerdefinierten Port starten |
| `yarn twenty server start --test` | Starten Sie eine separate Testinstanz auf Port 2021 |
| `yarn twenty server stop` | Server stoppen (Daten bleiben erhalten) |
| `yarn twenty server status` | Serverstatus, URL und Anmeldedaten anzeigen |
| `yarn twenty server logs` | Serverprotokolle streamen |
| `yarn twenty server logs --lines 100` | Die letzten 100 Protokollzeilen anzeigen |
| `yarn twenty server reset` | Alle Daten löschen und neu starten |
| Befehl | Beschreibung |
| -------------------------------------- | -------------------------------------------------------------------------------- |
| `yarn twenty server start` | Lokalen Server starten (lädt das Image bei Bedarf herunter) |
| `yarn twenty server start --port 3030` | Auf einem benutzerdefinierten Port starten |
| `yarn twenty server start --test` | Starten Sie eine separate Testinstanz auf Port 2021 |
| `yarn twenty server stop` | Server stoppen (Daten bleiben erhalten) |
| `yarn twenty server status` | Serverstatus, URL, Version und Anmeldedaten anzeigen |
| `yarn twenty server logs` | Serverprotokolle streamen |
| `yarn twenty server logs --lines 100` | Die letzten 100 Protokollzeilen anzeigen |
| `yarn twenty server reset` | Alle Daten löschen und neu starten |
| `yarn twenty server upgrade` | Das neueste `twenty-app-dev`-Image herunterladen und den Container neu erstellen |
| `yarn twenty server upgrade 2.2.0` | Auf eine bestimmte Version aktualisieren |
Daten bleiben über Neustarts hinweg in zwei Docker-Volumes bestehen (`twenty-app-dev-data` für PostgreSQL, `twenty-app-dev-storage` für Dateien). Verwenden Sie `reset`, um alles zu löschen und neu zu beginnen.
### Aktualisieren des Server-Images
Verwenden Sie `yarn twenty server upgrade`, um nach einem neueren `twenty-app-dev`-Docker-Image zu suchen und den Container zu aktualisieren. Der Befehl lädt das Image herunter, vergleicht es mit dem, aus dem der Container erstellt wurde, und erstellt den Container nur neu, wenn sich das Image tatsächlich geändert hat. Ihre Daten-Volumes bleiben erhalten — nur der Container wird ersetzt.
```bash filename="Terminal"
# Upgrade to the latest version (skips recreation if already up to date)
yarn twenty server upgrade
# Upgrade to a specific version
yarn twenty server upgrade 2.2.0
```
Wenn ein neueres Image verfügbar ist und der Container lief, startet der Upgrade-Befehl automatisch einen neuen Container mit dem aktualisierten Image. Führen Sie anschließend `yarn twenty server start` aus, um zu warten, bis er betriebsbereit ist. Wenn sich das Image nicht geändert hat, bleibt der Container unverändert.
Sie können die laufende Version mit `yarn twenty server status` überprüfen; dieser Befehl zeigt die `APP_VERSION` des Containers an.
### Eine Testinstanz ausführen
Übergeben Sie `--test` an jeden `server`-Befehl, um eine zweite, vollständig isolierte Instanz zu verwalten — nützlich, um Integrationstests auszuführen oder zu experimentieren, ohne Ihre Hauptentwicklungsdaten anzutasten.
| Befehl | Beschreibung |
| ---------------------------------- | ----------------------------------------------------- |
| `yarn twenty server start --test` | Die Testinstanz starten (standardmäßig Port 2021) |
| `yarn twenty server stop --test` | Die Testinstanz stoppen |
| `yarn twenty server status --test` | Status, URL und Anmeldedaten der Testinstanz anzeigen |
| `yarn twenty server logs --test` | Protokolle der Testinstanz streamen |
| `yarn twenty server reset --test` | Alle Testdaten löschen und neu starten |
| Befehl | Beschreibung |
| ----------------------------------- | -------------------------------------------------------------- |
| `yarn twenty server start --test` | Die Testinstanz starten (standardmäßig Port 2021) |
| `yarn twenty server stop --test` | Die Testinstanz stoppen |
| `yarn twenty server status --test` | Status, URL, Version und Anmeldedaten der Testinstanz anzeigen |
| `yarn twenty server logs --test` | Protokolle der Testinstanz streamen |
| `yarn twenty server reset --test` | Alle Testdaten löschen und neu starten |
| `yarn twenty server upgrade --test` | Das Image der Testinstanz aktualisieren |
Die Testinstanz läuft in einem eigenen Docker-Container (`twenty-app-dev-test`) mit dedizierten Volumes (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`) und eigener Konfiguration, sodass sie parallel zu Ihrer Hauptinstanz ohne Konflikte ausgeführt werden kann. Kombinieren Sie `--test` mit `--port`, um den Standardport 2021 zu überschreiben.
@@ -77,6 +77,39 @@ Pre-Release-Tags funktionieren wie erwartet: Das Erhöhen von `1.0.0-rc.1` → `
{/* TODO: add screenshot of the Upgrade button */}
### Kompatibilität der Serverversionen
Wenn Ihre App eine Funktion verwendet, die in einer bestimmten Twenty-Serverversion eingeführt wurde (z. B. OAuth-Anbieter, die in v2.3.0 hinzugefügt wurden), sollten Sie die minimale Serverversion, die Ihre App benötigt, mithilfe des Felds `engines.twenty` in `package.json` angeben:
```json filename="package.json"
{
"name": "twenty-my-app",
"version": "1.0.0",
"engines": {
"node": "^24.5.0",
"twenty": ">=2.3.0"
}
}
```
Der Wert ist ein standardmäßiger [semver-Bereich](https://github.com/npm/node-semver#ranges). Häufige Muster:
| Bereich | Bedeutung |
| ---------------------------------- | ------------------------------------------------------ |
| `>=2.3.0` | Jeder Server ab 2.3.0 |
| `>=2.3.0 \<3.0.0` | 2.3.0 oder höher, aber unter der nächsten Hauptversion |
| `^2.3.0` | Entspricht `>=2.3.0 \<3.0.0` |
**Was bei Bereitstellung und Installation passiert:**
* Wenn `engines.twenty` gesetzt ist und die Version des Zielservers den Bereich nicht erfüllt, wird die Bereitstellung (Tarball-Upload) oder Installation mit dem Fehler `SERVER_VERSION_INCOMPATIBLE` abgelehnt, zusammen mit einer Meldung, die sowohl den erforderlichen Bereich als auch die tatsächliche Serverversion angibt.
* Wenn `engines.twenty` nicht gesetzt ist, wird die App auf jeder Serverversion akzeptiert (abwärtskompatibel mit bestehenden Apps).
* Wenn auf dem Server keine `APP_VERSION` konfiguriert ist, wird die Prüfung übersprungen.
<Note>
Der Server ist die maßgebliche Prüfinstanz — er validiert `engines.twenty` sowohl beim Tarball-Upload als auch bei der Workspace-Installation. Auch wenn Sie ein Tarball außerhalb des regulären Prozesses bereitstellen oder aus dem Marketplace installieren, erzwingt der Server weiterhin die Kompatibilität.
</Note>
## Automatisiertes CI/CD (vorgefertigte Workflows)
Apps, die mit `create-twenty-app` erzeugt wurden, enthalten von Haus aus zwei GitHub-Actions-Workflows unter `.github/workflows/`. Sie sind einsatzbereit, sobald Sie das Repository zu GitHub pushen — für CI ist keine zusätzliche Einrichtung erforderlich, und für CD ist nur ein einziges Secret nötig.
@@ -0,0 +1,193 @@
---
title: Conexões
description: Permita que seu aplicativo aja em nome de um usuário em serviços de terceiros via OAuth.
icon: plug
---
Conexões são credenciais que um usuário mantém para um serviço externo (Linear, GitHub, Slack, ...). Seu app declara **como** essas credenciais são obtidas — um **provedor de conexão** — e as consome em tempo de execução para fazer chamadas autenticadas à API de terceiros.
Atualmente, apenas o OAuth 2.0 tem suporte. Tipos de credenciais futuros (tokens de acesso pessoal, chaves de API, autenticação básica) serão conectados à mesma interface — apps que já usam `defineConnectionProvider({ type: 'oauth', ... })` não precisarão migrar.
<AccordionGroup>
<Accordion title="defineConnectionProvider" description="Declare como as conexões do seu app são obtidas">
Um provedor de conexão descreve o handshake OAuth de que seu app precisa. O usuário clica em "Adicionar conexão" nas configurações do seu app, conclui a tela de consentimento do provedor e uma linha `ConnectedAccount` é criada no seu workspace.
Uma configuração funcional precisa de **dois arquivos** — o provedor de conexão e uma declaração correspondente de `serverVariables` em `defineApplication` que contém as credenciais do cliente OAuth.
```ts src/connection-providers/linear-connection.ts
import { defineConnectionProvider } from 'twenty-sdk/define';
export default defineConnectionProvider({
universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f',
name: 'linear',
displayName: 'Linear',
icon: 'IconBrandLinear',
type: 'oauth',
oauth: {
authorizationEndpoint: 'https://linear.app/oauth/authorize',
tokenEndpoint: 'https://api.linear.app/oauth/token',
scopes: ['read', 'write'],
// These must match keys in `defineApplication.serverVariables` below.
clientIdVariable: 'LINEAR_CLIENT_ID',
clientSecretVariable: 'LINEAR_CLIENT_SECRET',
// Optional: defaults to 'json'. Some providers (Linear, Slack) want
// 'form-urlencoded' for the token request.
tokenRequestContentType: 'form-urlencoded',
// Optional: defaults to true. Disable only if the provider rejects PKCE.
usePkce: false,
// Optional: extra query params on the authorize URL.
// authorizationParams: { prompt: 'consent' },
// Optional: provider's RFC 7009 token revocation endpoint, called on disconnect.
// revokeEndpoint: 'https://example.com/oauth/revoke',
},
});
```
```ts src/application.config.ts
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: '...',
displayName: 'Linear',
description: 'Connect Linear to Twenty.',
defaultRoleUniversalIdentifier: '...',
// OAuth client credentials live on the app registration (one OAuth app per
// Twenty server, configured by the admin) — not per-workspace. Declare them
// as serverVariables so the admin can fill them in once for all installs.
serverVariables: {
LINEAR_CLIENT_ID: {
description: 'OAuth client ID from your Linear OAuth application.',
isSecret: false,
isRequired: true,
},
LINEAR_CLIENT_SECRET: {
description: 'OAuth client secret from your Linear OAuth application.',
isSecret: true,
isRequired: true,
},
},
});
```
Pontos-chave:
* `name` é a string de identificador exclusivo usada em `listConnections({ providerName })` (kebab-case, deve corresponder a `^[a-z][a-z0-9-]*$`).
* `displayName` aparece na aba de configurações do app e na lista de ferramentas de IA.
* `clientIdVariable` / `clientSecretVariable` são **nomes**, não valores — devem corresponder às chaves declaradas em `defineApplication.serverVariables`. Os `client_id` e `client_secret` reais são inseridos pelo administrador do servidor por meio da interface de registro do app e nunca são versionados no seu repositório.
* Use `serverVariables` (não `applicationVariables`) — as credenciais OAuth são do servidor como um todo e há um app OAuth por servidor do Twenty.
* Até que ambos os `serverVariables` sejam preenchidos, a aba de configurações do app mostra uma dica "precisa de administrador do servidor" e o botão "Adicionar conexão" fica desativado.
* `type: 'oauth'` é o único valor compatível atualmente. O discriminador é compatível com versões futuras: tipos futuros (`'pat'`, `'api-key'`, ...) adicionarão novos blocos de subconfiguração ao lado de `oauth`.
O URL de callback do OAuth que seu provedor precisa adicionar à lista de permissões é:
```
https://<your-twenty-server>/apps/oauth/callback
```
</Accordion>
<Accordion title="listConnections / getConnection" description="Use conexões a partir de uma função de lógica">
Dentro de um handler de função de lógica, `listConnections({ providerName })` retorna as linhas `ConnectedAccount` deste app para o provedor fornecido, com tokens de acesso atualizados.
```ts src/logic-functions/handlers/create-linear-issue-handler.ts
import { listConnections } from 'twenty-sdk/logic-function';
export const createLinearIssueHandler = async (input: {
teamId?: string;
title?: string;
}) => {
if (!input.teamId || !input.title) {
return { success: false, error: 'teamId and title are required' };
}
const connections = await listConnections({ providerName: 'linear' });
// Workspace-shared credentials win when present; fall back to the first
// user-visibility one. For HTTP-route triggers you typically pick the
// request user's connection via event.userWorkspaceId instead.
const connection =
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
if (!connection) {
return {
success: false,
error:
'Linear is not connected. Open the app settings and click "Add connection".',
};
}
// Use connection.accessToken to call the third-party API.
const response = await fetch('https://api.linear.app/graphql', {
method: 'POST',
headers: {
Authorization: `Bearer ${connection.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`,
}),
});
return { success: response.ok };
};
```
Cada conexão tem:
| Campo | Descrição |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `id` | ID de linha exclusivo; passe para `getConnection(id)` para buscar novamente um único registro |
| `visibilidade` | `'user'` (privada para um membro do workspace) ou `'workspace'` (compartilhada com todos os membros) |
| `escopos` | Permissões OAuth concedidas pelo provedor de origem (distintas de `visibility` — não têm relação) |
| `userWorkspaceId` | O id de userWorkspace do proprietário — útil para selecionar "a conexão do usuário da requisição" em gatilhos de rota HTTP |
| `accessToken` | Token de acesso OAuth recente (atualizado automaticamente se estiver expirado) |
| `name` / `handle` | O nome de exibição da conexão (derivado automaticamente no callback do OAuth, renomeável pelo usuário) |
| `authFailedAt` | Definido quando a atualização mais recente falhou; o usuário deve reconectar |
Pontos-chave:
* Passe `{ providerName }` para filtrar por provedor; omita para obter todas as conexões que este app possui em todos os provedores.
* O servidor atualiza transparentemente o token de acesso antes de retornar. Seu handler sempre vê um token utilizável (ou `authFailedAt` definido).
* `getConnection(id)` é o equivalente de uma única linha.
</Accordion>
<Accordion title="Visibilidade por usuário vs. compartilhada no workspace" description="Como os usuários escolhem entre credenciais privadas e compartilhadas">
Quando um usuário clica em "Adicionar conexão", é solicitado que escolha uma visibilidade:
* **Apenas para mim** — a credencial é privada para o usuário que a conectou. Qualquer função de lógica chamada em seu nome (gatilho de rota HTTP com `isAuthRequired: true`) a vê; gatilhos cron e eventos de banco de dados não.
* **Compartilhada no workspace** — qualquer membro do workspace pode usar a credencial. Gatilhos de cron / banco de dados também a veem, pois não há um usuário da requisição.
Use a adequada para cada handler:
```ts
// HTTP-route trigger — prefer the request user's own connection.
const conn =
connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ??
connections.find((c) => c.visibility === 'workspace');
// Cron trigger — no request user; only shared credentials are sensible.
const conn = connections.find((c) => c.visibility === 'workspace');
```
Várias conexões por (usuário, provedor) são permitidas, então o mesmo usuário pode manter "Linear pessoal" e "Linear de trabalho" lado a lado.
</Accordion>
<Accordion title="Configuração única do provedor" description="Registre seu app OAuth no serviço de terceiros">
Para cada provedor de conexão, o administrador do servidor precisa primeiro registrar um app OAuth no serviço de terceiros.
1. Acesse as configurações de desenvolvedor do provedor (por exemplo, https://linear.app/settings/api/applications/new).
2. Defina a **URI de redirecionamento** como `\<SERVER_URL>/apps/oauth/callback`.
3. Copie o **ID do cliente** e o **Segredo do cliente** gerados.
4. Abra o app instalado no Twenty como administrador do servidor → defina os valores nos `serverVariables` correspondentes.
5. Os membros do workspace podem então adicionar conexões na seção **Conexões** de cada app.
</Accordion>
</AccordionGroup>
@@ -77,7 +77,6 @@ export default defineApplication({
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
displayName: 'My Twenty App',
description: 'My first Twenty app',
icon: 'IconWorld',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
@@ -88,11 +88,11 @@ yarn twenty dev --verbose
```
<Warning>
O modo de desenvolvimento só está disponível em instâncias do Twenty em modo de desenvolvimento (`NODE_ENV=development`). Instâncias de produção rejeitam solicitações de sincronização de desenvolvimento. Use `yarn twenty deploy` para fazer o deploy em servidores de produção — veja [Publicando aplicativos](/l/pt/developers/extend/apps/publishing) para detalhes.
O modo de desenvolvimento só está disponível em instâncias do Twenty em modo de desenvolvimento (`NODE_ENV=development`). Instâncias de produção rejeitam solicitações de sincronização de desenvolvimento. Use `yarn twenty deploy` seguido de `yarn twenty install` para publicar e instalar em servidores de produção — `deploy` publica no registro de aplicativos, enquanto `install` o instala em um determinado workspace. Veja [Publicação de aplicativos](/l/pt/developers/extend/apps/publishing) para detalhes.
</Warning>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="Saída do terminal no modo de desenvolvimento" />
<img src="/images/docs/developers/extends/apps/dev.png" alt="Saída do terminal no modo de desenvolvimento" />
</div>
#### Sincronização única com `yarn twenty dev --once`
@@ -127,13 +127,7 @@ Clique em **My twenty app** para abrir o seu **registro do aplicativo**. Um regi
Clique em **View installed app** para ver o aplicativo instalado. A aba **About** mostra a versão atual e as opções de gerenciamento:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Aplicativo instalado — aba About" />
</div>
Altere para a aba **Content** para ver tudo o que seu aplicativo oferece — objetos, campos, funções de lógica e agentes:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-4.png" alt="Aplicativo instalado — aba Content" />
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Aplicação instalada" />
</div>
Tudo pronto! Edite qualquer arquivo em `src/` e as alterações serão detectadas automaticamente.
@@ -213,30 +207,49 @@ Os exemplos são obtidos do diretório [twenty-apps/examples](https://github.com
A ferramenta de scaffolding já iniciou um servidor local do Twenty para você. Para gerenciá-lo depois, use `yarn twenty server`:
| Comando | Descrição |
| -------------------------------------- | ------------------------------------------------------ |
| `yarn twenty server start` | Inicia o servidor local (baixa a imagem se necessário) |
| `yarn twenty server start --port 3030` | Iniciar em uma porta personalizada |
| `yarn twenty server start --test` | Inicie uma instância de teste separada na porta 2021 |
| `yarn twenty server stop` | Interrompe o servidor (preserva os dados) |
| `yarn twenty server status` | Mostra o status do servidor, a URL e as credenciais |
| `yarn twenty server logs` | Transmite os logs do servidor |
| `yarn twenty server logs --lines 100` | Mostra as últimas 100 linhas de log |
| `yarn twenty server reset` | Exclui todos os dados e inicia do zero |
| Comando | Descrição |
| -------------------------------------- | ----------------------------------------------------------------- |
| `yarn twenty server start` | Inicia o servidor local (baixa a imagem se necessário) |
| `yarn twenty server start --port 3030` | Iniciar em uma porta personalizada |
| `yarn twenty server start --test` | Inicie uma instância de teste separada na porta 2021 |
| `yarn twenty server stop` | Interrompe o servidor (preserva os dados) |
| `yarn twenty server status` | Mostra o status do servidor, a URL, a versão e as credenciais |
| `yarn twenty server logs` | Transmite os logs do servidor |
| `yarn twenty server logs --lines 100` | Mostra as últimas 100 linhas de log |
| `yarn twenty server reset` | Exclui todos os dados e inicia do zero |
| `yarn twenty server upgrade` | Baixe a imagem mais recente `twenty-app-dev` e recrie o contêiner |
| `yarn twenty server upgrade 2.2.0` | Atualizar para uma versão específica |
Os dados são persistidos entre reinicializações em dois volumes do Docker (`twenty-app-dev-data` para PostgreSQL, `twenty-app-dev-storage` para arquivos). Use `reset` para apagar tudo e começar do zero.
### Atualizando a imagem do servidor
Use `yarn twenty server upgrade` para verificar se há uma imagem do Docker `twenty-app-dev` mais recente e atualizar o container. O comando baixa a imagem, a compara com aquela a partir da qual o container foi criado e só recria o container se a imagem realmente tiver sido alterada. Seus volumes de dados são preservados — apenas o container é substituído.
```bash filename="Terminal"
# Upgrade to the latest version (skips recreation if already up to date)
yarn twenty server upgrade
# Upgrade to a specific version
yarn twenty server upgrade 2.2.0
```
Se uma imagem mais recente estiver disponível e o container estiver em execução, o comando de atualização iniciará automaticamente um novo container com a imagem atualizada. Em seguida, execute `yarn twenty server start` para aguardar até que ele fique saudável. Se a imagem não tiver mudado, o container permanece inalterado.
Você pode verificar a versão em execução com `yarn twenty server status`, que exibe o `APP_VERSION` do container.
### Executando uma instância de teste
Passe `--test` para qualquer comando de `server` para gerenciar uma segunda instância totalmente isolada — útil para executar testes de integração ou experimentar sem tocar nos seus dados principais de desenvolvimento.
| Comando | Descrição |
| ---------------------------------- | ------------------------------------------------------------- |
| `yarn twenty server start --test` | Inicia a instância de teste (padrão: porta 2021) |
| `yarn twenty server stop --test` | Interrompe a instância de teste |
| `yarn twenty server status --test` | Mostra o status da instância de teste, a URL e as credenciais |
| `yarn twenty server logs --test` | Transmite os logs da instância de teste |
| `yarn twenty server reset --test` | Exclui os dados de teste e inicia do zero |
| Comando | Descrição |
| ----------------------------------- | ----------------------------------------------------------------------- |
| `yarn twenty server start --test` | Inicia a instância de teste (padrão: porta 2021) |
| `yarn twenty server stop --test` | Interrompe a instância de teste |
| `yarn twenty server status --test` | Mostra o status da instância de teste, a URL, a versão e as credenciais |
| `yarn twenty server logs --test` | Transmite os logs da instância de teste |
| `yarn twenty server reset --test` | Exclui os dados de teste e inicia do zero |
| `yarn twenty server upgrade --test` | Atualiza a imagem da instância de teste |
A instância de teste é executada em seu próprio contêiner Docker (`twenty-app-dev-test`) com volumes dedicados (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`) e configuração própria, para que possa ser executada em paralelo com sua instância principal sem conflitos. Combine `--test` com `--port` para substituir a porta padrão 2021.
@@ -77,6 +77,39 @@ Tags de pré-lançamento funcionam como esperado: incrementar `1.0.0-rc.1` → `
{/* TODO: add screenshot of the Upgrade button */}
### Compatibilidade da versão do servidor
Se o seu aplicativo usar um recurso introduzido em uma versão específica do servidor Twenty (por exemplo, provedores OAuth adicionados na v2.3.0), você deve declarar a versão mínima do servidor que seu aplicativo requer usando o campo `engines.twenty` em `package.json`:
```json filename="package.json"
{
"name": "twenty-my-app",
"version": "1.0.0",
"engines": {
"node": "^24.5.0",
"twenty": ">=2.3.0"
}
}
```
O valor é um [intervalo semver](https://github.com/npm/node-semver#ranges) padrão. Padrões comuns:
| Intervalo | Significado |
| ---------------------------------- | ---------------------------------------------------------- |
| `>=2.3.0` | Qualquer servidor a partir de 2.3.0 |
| `>=2.3.0 \<3.0.0` | 2.3.0 ou posterior, mas abaixo da próxima versão principal |
| `^2.3.0` | O mesmo que `>=2.3.0 \<3.0.0` |
**O que acontece no momento da implantação e da instalação:**
* Se `engines.twenty` estiver definido e a versão do servidor de destino não satisfizer o intervalo, a implantação (upload do tarball) ou a instalação será rejeitada com o erro `SERVER_VERSION_INCOMPATIBLE` e uma mensagem indicando tanto o intervalo exigido quanto a versão real do servidor.
* Se `engines.twenty` **não estiver definido**, o aplicativo é aceito em qualquer versão do servidor (retrocompatível com os aplicativos existentes).
* Se o servidor não tiver `APP_VERSION` configurado, a verificação será ignorada.
<Note>
O servidor realiza a verificação definitiva — ele valida `engines.twenty` tanto no upload do tarball quanto na instalação no workspace. Se você implantar um tarball fora de banda ou instalar a partir do marketplace, o servidor ainda impõe a compatibilidade.
</Note>
## CI/CD automatizado (fluxos de trabalho pré-configurados)
Os apps gerados com `create-twenty-app` já vêm com dois fluxos de trabalho do GitHub Actions prontos, em `.github/workflows/`. Eles estão prontos para executar assim que você fizer push do repositório para o GitHub — nenhuma configuração extra é necessária para CI, e CD requer apenas um único segredo.
@@ -0,0 +1,193 @@
---
title: Подключения
description: Разрешите вашему приложению действовать от имени пользователя в сторонних сервисах с помощью OAuth.
icon: plug
---
Подключения — это учетные данные, которыми пользователь располагает для внешнего сервиса (Linear, GitHub, Slack, ...). Ваше приложение определяет, **как** получают эти учетные данные — через **провайдера подключения** — и использует их во время выполнения для выполнения аутентифицированных вызовов к стороннему API.
На данный момент поддерживается только OAuth 2.0. Будущие типы учетных данных (персональные токены доступа, ключи API, базовая аутентификация) будут подключаться к тому же интерфейсу — приложения, уже использующие `defineConnectionProvider({ type: 'oauth', ... })` не потребуют миграции.
<AccordionGroup>
<Accordion title="defineConnectionProvider" description="Определите, как в вашем приложении получаются подключения">
Провайдер подключения описывает процедуру OAuth-обмена, которая требуется вашему приложению. Пользователь нажимает "Добавить подключение" в настройках вашего приложения, подтверждает разрешения на экране согласия провайдера, и в его рабочем пространстве создается запись `ConnectedAccount`.
Рабочей конфигурации нужны **два файла** — провайдер подключения и соответствующее объявление `serverVariables` в `defineApplication`, которое содержит учетные данные клиента OAuth.
```ts src/connection-providers/linear-connection.ts
import { defineConnectionProvider } from 'twenty-sdk/define';
export default defineConnectionProvider({
universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f',
name: 'linear',
displayName: 'Linear',
icon: 'IconBrandLinear',
type: 'oauth',
oauth: {
authorizationEndpoint: 'https://linear.app/oauth/authorize',
tokenEndpoint: 'https://api.linear.app/oauth/token',
scopes: ['read', 'write'],
// These must match keys in `defineApplication.serverVariables` below.
clientIdVariable: 'LINEAR_CLIENT_ID',
clientSecretVariable: 'LINEAR_CLIENT_SECRET',
// Optional: defaults to 'json'. Some providers (Linear, Slack) want
// 'form-urlencoded' for the token request.
tokenRequestContentType: 'form-urlencoded',
// Optional: defaults to true. Disable only if the provider rejects PKCE.
usePkce: false,
// Optional: extra query params on the authorize URL.
// authorizationParams: { prompt: 'consent' },
// Optional: provider's RFC 7009 token revocation endpoint, called on disconnect.
// revokeEndpoint: 'https://example.com/oauth/revoke',
},
});
```
```ts src/application.config.ts
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: '...',
displayName: 'Linear',
description: 'Connect Linear to Twenty.',
defaultRoleUniversalIdentifier: '...',
// OAuth client credentials live on the app registration (one OAuth app per
// Twenty server, configured by the admin) — not per-workspace. Declare them
// as serverVariables so the admin can fill them in once for all installs.
serverVariables: {
LINEAR_CLIENT_ID: {
description: 'OAuth client ID from your Linear OAuth application.',
isSecret: false,
isRequired: true,
},
LINEAR_CLIENT_SECRET: {
description: 'OAuth client secret from your Linear OAuth application.',
isSecret: true,
isRequired: true,
},
},
});
```
Основные моменты:
* `name` — это уникальная строка-идентификатор, используемая в `listConnections({ providerName })` (kebab-case, должна соответствовать `^[a-z][a-z0-9-]*$`).
* `displayName` отображается на вкладке настроек приложения и в списке инструментов ИИ.
* `clientIdVariable` / `clientSecretVariable` — это **имена**, а не значения — они должны совпадать с ключами, объявленными в `defineApplication.serverVariables`. Фактические `client_id` и `client_secret` вводятся администратором сервера через интерфейс регистрации приложения и никогда не коммитятся в ваш репозиторий.
* Используйте `serverVariables` (не `applicationVariables`) — учетные данные OAuth являются общими для сервера, и на каждом сервере Twenty используется одно приложение OAuth.
* Пока оба `serverVariables` не заполнены, на вкладке настроек приложения показывается подсказка "нужен администратор сервера", а кнопка "Добавить подключение" отключена.
* `type: 'oauth'` — единственное поддерживаемое сегодня значение. Дискриминатор совместим с будущими версиями: будущие типы (`'pat'`, `'api-key'`, ...) добавят новые блоки подконфигурации рядом с `oauth`.
URL обратного вызова OAuth, который вашему провайдеру нужно добавить в список разрешенных:
```
https://<your-twenty-server>/apps/oauth/callback
```
</Accordion>
<Accordion title="listConnections / getConnection" description="Используйте подключения из логической функции">
Внутри обработчика логической функции `listConnections({ providerName })` возвращает записи `ConnectedAccount` этого приложения для указанного провайдера с обновленными токенами доступа.
```ts src/logic-functions/handlers/create-linear-issue-handler.ts
import { listConnections } from 'twenty-sdk/logic-function';
export const createLinearIssueHandler = async (input: {
teamId?: string;
title?: string;
}) => {
if (!input.teamId || !input.title) {
return { success: false, error: 'teamId and title are required' };
}
const connections = await listConnections({ providerName: 'linear' });
// Workspace-shared credentials win when present; fall back to the first
// user-visibility one. For HTTP-route triggers you typically pick the
// request user's connection via event.userWorkspaceId instead.
const connection =
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
if (!connection) {
return {
success: false,
error:
'Linear is not connected. Open the app settings and click "Add connection".',
};
}
// Use connection.accessToken to call the third-party API.
const response = await fetch('https://api.linear.app/graphql', {
method: 'POST',
headers: {
Authorization: `Bearer ${connection.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`,
}),
});
return { success: response.ok };
};
```
Каждое подключение имеет:
| Поле | Описание |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `id` | Уникальный идентификатор записи; передайте его в `getConnection(id)`, чтобы повторно получить одну запись |
| `visibility` | `'user'` (приватно для одного участника рабочего пространства) или `'workspace'` (доступно всем участникам) |
| `scopes` | Разрешения OAuth, предоставленные внешним провайдером (отличаются от `visibility` — это несвязанные вещи) |
| `userWorkspaceId` | Идентификатор userWorkspace владельца — полезно для выбора "подключения пользователя запроса" в триггерах HTTP-маршрутов |
| `accessToken` | Актуальный токен доступа OAuth (обновляется автоматически при истечении срока действия) |
| `name` / `handle` | Отображаемое имя подключения (автоматически определяется при обратном вызове OAuth, может быть переименовано пользователем) |
| `authFailedAt` | Устанавливается, если последняя попытка обновления не удалась; пользователю нужно переподключиться |
Основные моменты:
* Передайте `{ providerName }`, чтобы отфильтровать по провайдеру; опустите, чтобы получить все подключения этого приложения у всех провайдеров.
* Сервер прозрачно обновляет токен доступа перед возвратом. Ваш обработчик всегда получает рабочий токен (или установлено `authFailedAt`).
* `getConnection(id)` — эквивалент для одной записи.
</Accordion>
<Accordion title="Индивидуальная и общая для рабочего пространства видимость" description="Как пользователи выбирают между приватными и общими учетными данными">
Когда пользователь нажимает "Добавить подключение", ему предлагается выбрать видимость:
* **Только для меня** — учетные данные приватны для подключившегося пользователя. Любая логическая функция, вызываемая от его имени (триггер HTTP-маршрута с `isAuthRequired: true`), видит их; триггеры cron и события базы данных — нет.
* **Общее для рабочего пространства** — любой участник рабочего пространства может использовать эти учетные данные. Триггеры cron/базы данных также видят их, поскольку у них нет пользователя запроса.
Используйте подходящий вариант для каждого обработчика:
```ts
// HTTP-route trigger — prefer the request user's own connection.
const conn =
connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ??
connections.find((c) => c.visibility === 'workspace');
// Cron trigger — no request user; only shared credentials are sensible.
const conn = connections.find((c) => c.visibility === 'workspace');
```
Допускается несколько подключений на пару (пользователь, провайдер), поэтому один и тот же пользователь может иметь "Personal Linear" и "Work Linear" одновременно.
</Accordion>
<Accordion title="Единоразовая настройка провайдера" description="Зарегистрируйте свое приложение OAuth у стороннего сервиса">
Для каждого провайдера подключения администратору сервера сначала нужно зарегистрировать у стороннего сервиса приложение OAuth.
1. Перейдите в настройки разработчика провайдера (например, https://linear.app/settings/api/applications/new).
2. Установите **Redirect URI** в значение `\<SERVER_URL>/apps/oauth/callback`.
3. Скопируйте сгенерированные **Client ID** и **Client Secret**.
4. Откройте установленное приложение в Twenty под учетной записью администратора сервера → задайте значения в соответствующих `serverVariables`.
5. Затем участники рабочего пространства смогут добавлять подключения в разделе **Подключения** конкретного приложения.
</Accordion>
</AccordionGroup>
@@ -77,7 +77,6 @@ export default defineApplication({
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
displayName: 'My Twenty App',
description: 'My first Twenty app',
icon: 'IconWorld',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
@@ -88,11 +88,11 @@ yarn twenty dev --verbose
```
<Warning>
Режим разработки доступен только на экземплярах Twenty, запущенных в режиме разработки (`NODE_ENV=development`). Экземпляры в продакшене отклоняют запросы синхронизации из режима разработки. Используйте `yarn twenty deploy` для развёртывания на продакшен-серверах — подробности см. в разделе [Публикация приложений](/l/ru/developers/extend/apps/publishing).
Режим разработки доступен только на экземплярах Twenty, запущенных в режиме разработки (`NODE_ENV=development`). Экземпляры в продакшене отклоняют запросы синхронизации из режима разработки. Используйте `yarn twenty deploy`, а затем `yarn twenty install`, чтобы выполнить публикацию и установку на продакшн-серверах — `deploy` публикует в реестр приложений, а `install` устанавливает его в указанное рабочее пространство. См. [Публикация приложений](/l/ru/developers/extend/apps/publishing) для подробностей.
</Warning>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="Вывод терминала в режиме разработки" />
<img src="/images/docs/developers/extends/apps/dev.png" alt="Вывод терминала в режиме разработки" />
</div>
#### Разовая синхронизация с `yarn twenty dev --once`
@@ -127,13 +127,7 @@ yarn twenty dev --once
Нажмите **View installed app**, чтобы посмотреть установленное приложение. Вкладка **About** показывает текущую версию и параметры управления:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Установленное приложение — вкладка About" />
</div>
Переключитесь на вкладку **Content**, чтобы увидеть всё, что предоставляет ваше приложение: объекты, поля, логические функции и агенты:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-4.png" alt="Установленное приложение — вкладка Content" />
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Установленное приложение" />
</div>
Готово! Отредактируйте любой файл в `src/`, и изменения будут подхвачены автоматически.
@@ -213,30 +207,49 @@ npx create-twenty-app@latest my-twenty-app --example postcard
Генератор каркаса уже запустил для вас локальный сервер Twenty. Чтобы управлять им позже, используйте `yarn twenty server`:
| Команда | Описание |
| -------------------------------------- | -------------------------------------------------------------- |
| `yarn twenty server start` | Запустить локальный сервер (при необходимости скачивает образ) |
| `yarn twenty server start --port 3030` | Запустить на пользовательском порту |
| `yarn twenty server start --test` | Запускает отдельный тестовый экземпляр на порту 2021 |
| `yarn twenty server stop` | Остановить сервер (данные сохраняются) |
| `yarn twenty server status` | Показать состояние сервера, URL и учётные данные |
| `yarn twenty server logs` | Потоковый вывод журналов сервера |
| `yarn twenty server logs --lines 100` | Показать последние 100 строк журнала |
| `yarn twenty server reset` | Удалить все данные и начать с чистого листа |
| Команда | Описание |
| -------------------------------------- | ------------------------------------------------------------------ |
| `yarn twenty server start` | Запустить локальный сервер (при необходимости скачивает образ) |
| `yarn twenty server start --port 3030` | Запустить на пользовательском порту |
| `yarn twenty server start --test` | Запускает отдельный тестовый экземпляр на порту 2021 |
| `yarn twenty server stop` | Остановить сервер (данные сохраняются) |
| `yarn twenty server status` | Показать состояние сервера, URL, версию и учётные данные |
| `yarn twenty server logs` | Потоковый вывод журналов сервера |
| `yarn twenty server logs --lines 100` | Показать последние 100 строк журнала |
| `yarn twenty server reset` | Удалить все данные и начать с чистого листа |
| `yarn twenty server upgrade` | Загрузить последний образ `twenty-app-dev` и пересоздать контейнер |
| `yarn twenty server upgrade 2.2.0` | Обновить до конкретной версии |
Данные сохраняются между перезапусками в двух томах Docker (`twenty-app-dev-data` для PostgreSQL, `twenty-app-dev-storage` для файлов). Используйте `reset`, чтобы стереть всё и начать заново.
### Обновление образа сервера
Используйте `yarn twenty server upgrade`, чтобы проверить наличие более нового Docker-образа `twenty-app-dev` и обновить контейнер. Команда скачивает образ, сравнивает его с тем, из которого был создан контейнер, и пересоздаёт контейнер только если образ действительно изменился. Ваши тома данных сохраняются — заменяется только контейнер.
```bash filename="Terminal"
# Upgrade to the latest version (skips recreation if already up to date)
yarn twenty server upgrade
# Upgrade to a specific version
yarn twenty server upgrade 2.2.0
```
Если доступен более новый образ и контейнер был запущен, команда обновления автоматически запускает новый контейнер с обновлённым образом. Затем выполните `yarn twenty server start`, чтобы дождаться перехода контейнера в состояние healthy. Если образ не изменился, контейнер остаётся без изменений.
Вы можете проверить запущенную версию с помощью `yarn twenty server status` — эта команда показывает `APP_VERSION` из контейнера.
### Запуск тестового экземпляра
Передайте `--test` любой команде `server`, чтобы управлять вторым, полностью изолированным экземпляром — это полезно для запуска интеграционных тестов или экспериментов, не затрагивая ваши основные данные разработки.
| Команда | Описание |
| ---------------------------------- | ------------------------------------------------------------- |
| `yarn twenty server start --test` | Запустить тестовый экземпляр (по умолчанию — порт 2021) |
| `yarn twenty server stop --test` | Остановить тестовый экземпляр |
| `yarn twenty server status --test` | Показать состояние тестового экземпляра, URL и учётные данные |
| `yarn twenty server logs --test` | Выводить журналы тестового экземпляра в потоковом режиме |
| `yarn twenty server reset --test` | Удалить тестовые данные и начать с чистого листа |
| Команда | Описание |
| ----------------------------------- | --------------------------------------------------------------------- |
| `yarn twenty server start --test` | Запустить тестовый экземпляр (по умолчанию — порт 2021) |
| `yarn twenty server stop --test` | Остановить тестовый экземпляр |
| `yarn twenty server status --test` | Показать состояние тестового экземпляра, URL, версию и учётные данные |
| `yarn twenty server logs --test` | Выводить журналы тестового экземпляра в потоковом режиме |
| `yarn twenty server reset --test` | Удалить тестовые данные и начать с чистого листа |
| `yarn twenty server upgrade --test` | Обновить образ тестового экземпляра |
Тестовый экземпляр запускается в собственном контейнере Docker (`twenty-app-dev-test`) с выделенными томами (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`) и собственной конфигурацией, поэтому он может работать параллельно с вашим основным экземпляром без конфликтов. Совместите `--test` с `--port`, чтобы переопределить значение по умолчанию (2021).
@@ -77,6 +77,39 @@ When updating an already deployed tarball app, the server requires the `version`
{/* TODO: add screenshot of the Upgrade button */}
### Совместимость версий сервера
Если ваше приложение использует функцию, появившуюся в конкретной версии сервера Twenty (например, провайдеры OAuth, добавленные в v2.3.0), следует объявить минимальную требуемую версию сервера с помощью поля `engines.twenty` в `package.json`:
```json filename="package.json"
{
"name": "twenty-my-app",
"version": "1.0.0",
"engines": {
"node": "^24.5.0",
"twenty": ">=2.3.0"
}
}
```
Значение — это стандартный [диапазон semver](https://github.com/npm/node-semver#ranges). Типовые шаблоны:
| Диапазон | Значение |
| ---------------------------------- | -------------------------------------------------- |
| `>=2.3.0` | Любой сервер версии 2.3.0 и новее |
| `>=2.3.0 \<3.0.0` | 2.3.0 или новее, но ниже следующей мажорной версии |
| `^2.3.0` | То же, что и `>=2.3.0 \<3.0.0` |
**Что происходит при развёртывании и установке:**
* Если `engines.twenty` задано и версия целевого сервера не удовлетворяет диапазону, развёртывание (загрузка tarball-архива) или установка отклоняются с ошибкой `SERVER_VERSION_INCOMPATIBLE` и сообщением, указывающим как требуемый диапазон, так и фактическую версию сервера.
* Если `engines.twenty` **не задано**, приложение принимается на сервере любой версии (обратная совместимость с существующими приложениями).
* Если на сервере `APP_VERSION` не задано, проверка пропускается.
<Note>
Сервер выполняет окончательную проверку — он проверяет `engines.twenty` как при загрузке tarball-архива, так и при установке в рабочем пространстве. Если вы развёртываете tarball вне стандартного процесса или устанавливаете из маркетплейса, сервер всё равно принудительно проверяет совместимость.
</Note>
## Автоматизированный CI/CD (рабочие процессы, сгенерированные шаблоном)
Приложения, созданные с помощью `create-twenty-app`, «из коробки» включают два рабочих процесса GitHub Actions в каталоге `.github/workflows/`. Они готовы к запуску, как только вы запушите репозиторий на GitHub — для CI не требуется дополнительной настройки, а для CD нужен лишь один секрет.
@@ -0,0 +1,193 @@
---
title: Bağlantılar
description: Uygulamanızın, OAuth aracılığıyla üçüncü taraf hizmetlerde kullanıcı adına işlem yapmasına izin verin.
icon: plug
---
Bağlantılar, bir kullanıcının harici bir hizmet için (Linear, GitHub, Slack, ...) sahip olduğu kimlik bilgileridir. Uygulamanız bu kimlik bilgilerinin **nasıl** elde edildiğini — bir **bağlantı sağlayıcısı** — bildirir ve çalışma zamanında üçüncü taraf API'sine kimlik doğrulamalı çağrılar yapmak için bunları kullanır.
Bugün yalnızca OAuth 2.0 destekleniyor. Gelecekteki kimlik bilgisi türleri (kişisel erişim belirteçleri, API anahtarları, basic auth) aynı yüzeye bağlanacak — halihazırda `defineConnectionProvider({ type: 'oauth', ... })` kullanan uygulamaların geçiş yapması gerekmeyecek.
<AccordionGroup>
<Accordion title="defineConnectionProvider" description="Uygulamanızın bağlantılarının nasıl elde edildiğini belirtin">
Bir bağlantı sağlayıcısı, uygulamanızın ihtiyaç duyduğu OAuth el sıkışmasını açıklar. Kullanıcı, uygulamanızın ayarlarında "Bağlantı ekle"ye tıklar, sağlayıcının izin ekranını tamamlar ve çalışma alanında bir `ConnectedAccount` satırı oluşturulur.
Çalışan bir kurulum **iki dosya** gerektirir — bağlantı sağlayıcısı ve OAuth istemci kimlik bilgilerini tutan `defineApplication` üzerindeki eşleşen bir `serverVariables` bildirimi.
```ts src/connection-providers/linear-connection.ts
import { defineConnectionProvider } from 'twenty-sdk/define';
export default defineConnectionProvider({
universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f',
name: 'linear',
displayName: 'Linear',
icon: 'IconBrandLinear',
type: 'oauth',
oauth: {
authorizationEndpoint: 'https://linear.app/oauth/authorize',
tokenEndpoint: 'https://api.linear.app/oauth/token',
scopes: ['read', 'write'],
// These must match keys in `defineApplication.serverVariables` below.
clientIdVariable: 'LINEAR_CLIENT_ID',
clientSecretVariable: 'LINEAR_CLIENT_SECRET',
// Optional: defaults to 'json'. Some providers (Linear, Slack) want
// 'form-urlencoded' for the token request.
tokenRequestContentType: 'form-urlencoded',
// Optional: defaults to true. Disable only if the provider rejects PKCE.
usePkce: false,
// Optional: extra query params on the authorize URL.
// authorizationParams: { prompt: 'consent' },
// Optional: provider's RFC 7009 token revocation endpoint, called on disconnect.
// revokeEndpoint: 'https://example.com/oauth/revoke',
},
});
```
```ts src/application.config.ts
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: '...',
displayName: 'Linear',
description: 'Connect Linear to Twenty.',
defaultRoleUniversalIdentifier: '...',
// OAuth client credentials live on the app registration (one OAuth app per
// Twenty server, configured by the admin) — not per-workspace. Declare them
// as serverVariables so the admin can fill them in once for all installs.
serverVariables: {
LINEAR_CLIENT_ID: {
description: 'OAuth client ID from your Linear OAuth application.',
isSecret: false,
isRequired: true,
},
LINEAR_CLIENT_SECRET: {
description: 'OAuth client secret from your Linear OAuth application.',
isSecret: true,
isRequired: true,
},
},
});
```
Önemli noktalar:
* `name`, `listConnections({ providerName })` içinde kullanılan benzersiz tanımlayıcı dizedir (kebab-case, `^[a-z][a-z0-9-]*$` ile eşleşmelidir).
* `displayName` uygulama başına ayarlar sekmesinde ve Yapay Zeka araç listesinde gösterilir.
* `clientIdVariable` / `clientSecretVariable` değer değil, **isimdir** — `defineApplication.serverVariables` içinde bildirilen anahtarlarla eşleşmelidir. Gerçek `client_id` ve `client_secret`, sunucu yöneticisi tarafından uygulama kayıt arayüzü üzerinden girilir; deponuza asla commit edilmez.
* `serverVariables` kullanın (`applicationVariables` değil) — OAuth kimlik bilgileri sunucu genelidir ve her Twenty sunucusu için bir OAuth uygulaması vardır.
* Her iki `serverVariables` da doldurulana kadar, uygulama başına ayarlar sekmesi "sunucu yöneticisine ihtiyaç var" ipucunu gösterir ve "Bağlantı ekle" düğmesi devre dışı bırakılır.
* `type: 'oauth'` bugün desteklenen tek değerdir. Seçici ileriye dönük uyumludur: gelecekteki türler (`'pat'`, `'api-key'`, ...) `oauth` yanında yeni alt yapılandırma blokları eklenecektir.
Sağlayıcınızın beyaz listeye alması gereken OAuth geri çağrı URL'si şudur:
```
https://<your-twenty-server>/apps/oauth/callback
```
</Accordion>
<Accordion title="listConnections / getConnection" description="Bir mantık işlevinden bağlantıları kullanın">
Bir mantık işlevi işleyicisi içinde, `listConnections({ providerName })`, verilen sağlayıcı için bu uygulamanın `ConnectedAccount` satırlarını, yenilenmiş erişim belirteçleriyle döndürür.
```ts src/logic-functions/handlers/create-linear-issue-handler.ts
import { listConnections } from 'twenty-sdk/logic-function';
export const createLinearIssueHandler = async (input: {
teamId?: string;
title?: string;
}) => {
if (!input.teamId || !input.title) {
return { success: false, error: 'teamId and title are required' };
}
const connections = await listConnections({ providerName: 'linear' });
// Workspace-shared credentials win when present; fall back to the first
// user-visibility one. For HTTP-route triggers you typically pick the
// request user's connection via event.userWorkspaceId instead.
const connection =
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
if (!connection) {
return {
success: false,
error:
'Linear is not connected. Open the app settings and click "Add connection".',
};
}
// Use connection.accessToken to call the third-party API.
const response = await fetch('https://api.linear.app/graphql', {
method: 'POST',
headers: {
Authorization: `Bearer ${connection.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`,
}),
});
return { success: response.ok };
};
```
Her bağlantı şunlara sahiptir:
| Alan | Açıklama |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `id` | Tekil satır kimliği; tek bir tanesini yeniden getirmek için `getConnection(id)` işlevine iletin |
| `visibility` | `'user'` (bir çalışma alanı üyesine özel) veya `'workspace'` (tüm üyelerle paylaşılan) |
| `scopes` | Üst sağlayıcı tarafından verilen OAuth izinleri (`visibility` ile karıştırılmamalıdır — bunlar ilişkili değildir) |
| `userWorkspaceId` | Sahibinin userWorkspace kimliği — HTTP rota tetikleyicilerinde "istek kullanıcısının bağlantısını" seçmek için kullanışlıdır |
| `accessToken` | Yeni OAuth erişim belirteci (süresi dolmuşsa otomatik olarak yenilenir) |
| `name` / `handle` | Bağlantının görünen adı (OAuth geri çağrısında otomatik türetilir, kullanıcı tarafından yeniden adlandırılabilir) |
| `authFailedAt` | En son yenileme başarısız olduğunda ayarlanır; kullanıcı yeniden bağlanmalıdır |
Önemli noktalar:
* Sağlayıcıya göre filtrelemek için `{ providerName }` iletin; bu uygulamanın tüm sağlayıcılardaki tüm bağlantılarını almak için bunu atlayın.
* Sunucu, döndürmeden önce erişim belirtecini şeffaf bir şekilde yeniler. İşleyiciniz her zaman kullanılabilir bir belirteç görür (veya `authFailedAt` ayarlanmıştır).
* `getConnection(id)`, tek satırlık karşılığıdır.
</Accordion>
<Accordion title="Kullanıcıya özel ve çalışma alanı paylaşımlı görünürlük" description="Kullanıcıların özel ve paylaşılan kimlik bilgileri arasında nasıl seçim yaptığı">
Bir kullanıcı "Bağlantı ekle"ye tıkladığında, bir görünürlük seçmesi istenir:
* **Yalnızca benim için** — kimlik bilgisi, bağlanan kullanıcıya özeldir. Adlarına çağrılan herhangi bir mantık işlevi (`isAuthRequired: true` ile HTTP rota tetikleyicisi) bunu görür; cron tetikleyicileri ve veritabanı olayları görmez.
* **Çalışma alanı paylaşımlı** — herhangi bir çalışma alanı üyesi bu kimlik bilgisini kullanabilir. Cron / veritabanı tetikleyicileri de görür, çünkü istek kullanıcısı yoktur.
Her işleyici için doğru olanı kullanın:
```ts
// HTTP-route trigger — prefer the request user's own connection.
const conn =
connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ??
connections.find((c) => c.visibility === 'workspace');
// Cron trigger — no request user; only shared credentials are sensible.
const conn = connections.find((c) => c.visibility === 'workspace');
```
Kullanıcı ve sağlayıcı başına birden çok bağlantıya izin verilir; böylece aynı kullanıcı "Personal Linear" ve "Work Linear" bağlantılarını yan yana tutabilir.
</Accordion>
<Accordion title="Tek seferlik sağlayıcı kurulumu" description="OAuth uygulamanızı üçüncü taraf hizmete kaydedin">
Her bağlantı sağlayıcısı için, sunucu yöneticisinin önce üçüncü tarafta bir OAuth uygulaması kaydetmesi gerekir.
1. Sağlayıcının geliştirici ayarlarına gidin (örn. https://linear.app/settings/api/applications/new).
2. **Redirect URI**'yi `\<SERVER_URL>/apps/oauth/callback` olarak ayarlayın.
3. Oluşturulan **Client ID** ve **Client Secret**'ı kopyalayın.
4. Yüklü uygulamayı Twenty'de bir sunucu yöneticisi olarak açın → karşılık gelen `serverVariables` üzerinde değerleri ayarlayın.
5. Ardından çalışma alanı üyeleri, uygulama başına **Bağlantılar** bölümünden bağlantılar ekleyebilir.
</Accordion>
</AccordionGroup>
@@ -77,7 +77,6 @@ export default defineApplication({
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
displayName: 'My Twenty App',
description: 'My first Twenty app',
icon: 'IconWorld',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
@@ -88,11 +88,11 @@ yarn twenty dev --verbose
```
<Warning>
Geliştirme modu yalnızca geliştirme ortamında (`NODE_ENV=development`) çalışan Twenty örneklerinde kullanılabilir. Üretim örnekleri geliştirme eşitleme isteklerini reddeder. Üretim sunucularına dağıtmak için `yarn twenty deploy` komutunu kullanın — ayrıntılar için [Uygulamaları Yayınlama](/l/tr/developers/extend/apps/publishing) bölümüne bakın.
Geliştirme modu yalnızca geliştirme ortamında (`NODE_ENV=development`) çalışan Twenty örneklerinde kullanılabilir. Üretim örnekleri geliştirme eşitleme isteklerini reddeder. Canlı sunucularda yayımlamak ve yüklemek için önce `yarn twenty deploy`, ardından `yarn twenty install` komutlarını kullanın — `deploy` uygulama kayıt defterine yayımlar, `install` ise bunu belirli bir çalışma alanına yükler. Ayrıntılar için [Uygulamaları Yayımlama](/l/tr/developers/extend/apps/publishing) bölümüne bakın.
</Warning>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="Geliştirme modu terminal çıktısı" />
<img src="/images/docs/developers/extends/apps/dev.png" alt="Geliştirme modu terminal çıktısı" />
</div>
#### Tek seferlik eşitleme `yarn twenty dev --once` ile
@@ -127,13 +127,7 @@ Tarayıcınızda [http://localhost:2020/settings/applications#developer](http://
Yüklü uygulamayı görmek için **View installed app**'e tıklayın. **About** sekmesi, geçerli sürümü ve yönetim seçeneklerini gösterir:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Yüklü uygulama — About sekmesi" />
</div>
Uygulamanızın sağladığı her şeyi — nesneler, alanlar, mantık işlevleri ve ajanlar — görmek için **Content** sekmesine geçin:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-4.png" alt="Yüklü uygulama — Content sekmesi" />
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Yüklü uygulama" />
</div>
Her şey hazır! `src/` içindeki herhangi bir dosyayı düzenleyin; değişiklikler otomatik olarak alınacaktır.
@@ -213,30 +207,49 @@ npx create-twenty-app@latest my-twenty-app --example postcard
İskelet oluşturucu sizin için zaten yerel bir Twenty sunucusu başlattı. Daha sonra yönetmek için `yarn twenty server` komutunu kullanın:
| Komut | Açıklama |
| -------------------------------------- | --------------------------------------------------------------- |
| `yarn twenty server start` | Yerel sunucuyu başlatır (gerekirse imajı çeker) |
| `yarn twenty server start --port 3030` | Özel bir portta başlatır |
| `yarn twenty server start --test` | 2021 numaralı bağlantı noktasında ayrı bir test örneği başlatın |
| `yarn twenty server stop` | Sunucuyu durdurur (verileri korur) |
| `yarn twenty server status` | Sunucu durumunu, URL'yi ve kimlik bilgilerini gösterir |
| `yarn twenty server logs` | Sunucu günlüklerini akış olarak iletir |
| `yarn twenty server logs --lines 100` | Son 100 günlük satırını gösterir |
| `yarn twenty server reset` | Tüm verileri siler ve sıfırdan başlatır |
| Komut | Açıklama |
| -------------------------------------- | --------------------------------------------------------------------- |
| `yarn twenty server start` | Yerel sunucuyu başlatır (gerekirse imajı çeker) |
| `yarn twenty server start --port 3030` | Özel bir portta başlatır |
| `yarn twenty server start --test` | 2021 numaralı bağlantı noktasında ayrı bir test örneği başlatın |
| `yarn twenty server stop` | Sunucuyu durdurur (verileri korur) |
| `yarn twenty server status` | Sunucu durumunu, URL'yi, sürümü ve kimlik bilgilerini gösterir |
| `yarn twenty server logs` | Sunucu günlüklerini akış olarak iletir |
| `yarn twenty server logs --lines 100` | Son 100 günlük satırını gösterir |
| `yarn twenty server reset` | Tüm verileri siler ve sıfırdan başlatır |
| `yarn twenty server upgrade` | En son `twenty-app-dev` imajını çeker ve konteyneri yeniden oluşturur |
| `yarn twenty server upgrade 2.2.0` | Belirli bir sürüme yükseltin |
Veriler, yeniden başlatmalar arasında iki Docker biriminde kalıcıdır (PostgreSQL için `twenty-app-dev-data`, dosyalar için `twenty-app-dev-storage`). Her şeyi silip baştan başlamak için `reset` kullanın.
### Sunucu imajını yükseltme
Daha yeni bir `twenty-app-dev` Docker imajı olup olmadığını kontrol etmek ve konteyneri güncellemek için `yarn twenty server upgrade` komutunu kullanın. Komut imajı çeker, konteynerin oluşturulduğu imajla karşılaştırır ve imaj gerçekten değiştiyse yalnızca konteyneri yeniden oluşturur. Veri birimleriniz korunur — yalnızca konteyner değiştirilir.
```bash filename="Terminal"
# Upgrade to the latest version (skips recreation if already up to date)
yarn twenty server upgrade
# Upgrade to a specific version
yarn twenty server upgrade 2.2.0
```
Daha yeni bir imaj mevcutsa ve konteyner çalışıyorsa, yükseltme komutu güncellenmiş imajla yeni bir konteyneri otomatik olarak başlatır. Ardından sağlıklı hale gelmesini beklemek için `yarn twenty server start` komutunu çalıştırın. İmaj değişmediyse, konteyner olduğu gibi bırakılır.
`yarn twenty server status` ile çalışan sürümü doğrulayabilirsiniz; bu komut, konteynerden `APP_VERSION` değerini görüntüler.
### Test örneği çalıştırma
`server` komutlarının herhangi birine `--test` parametresini vererek ikinci, tamamen yalıtılmış bir örneği yönetin — entegrasyon testlerini çalıştırmak veya ana geliştirme verilerinize dokunmadan denemeler yapmak için kullanışlıdır.
| Komut | Açıklama |
| ---------------------------------- | -------------------------------------------------------------- |
| `yarn twenty server start --test` | Test örneğini başlatır (varsayılan bağlantı noktası 2021'dir) |
| `yarn twenty server stop --test` | Test örneğini durdurur |
| `yarn twenty server status --test` | Test örneğinin durumunu, URL'yi ve kimlik bilgilerini gösterir |
| `yarn twenty server logs --test` | Test örneği günlüklerini akış olarak iletir |
| `yarn twenty server reset --test` | Test verilerini siler ve sıfırdan başlatır |
| Komut | Açıklama |
| ----------------------------------- | ---------------------------------------------------------------------- |
| `yarn twenty server start --test` | Test örneğini başlatır (varsayılan bağlantı noktası 2021'dir) |
| `yarn twenty server stop --test` | Test örneğini durdurur |
| `yarn twenty server status --test` | Test örneğinin durumunu, URL'yi, sürümü ve kimlik bilgilerini gösterir |
| `yarn twenty server logs --test` | Test örneği günlüklerini akış olarak iletir |
| `yarn twenty server reset --test` | Test verilerini siler ve sıfırdan başlatır |
| `yarn twenty server upgrade --test` | Test örneğinin imajını yükseltin |
Test örneği, kendine ait bir Docker konteynerinde (`twenty-app-dev-test`), ayrılmış birimlerle (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`) ve yapılandırmayla çalışır; böylece ana örneğinizle çakışma olmadan paralel olarak çalışabilir. Varsayılan 2021'i geçersiz kılmak için `--test` ile `--port`'u birlikte kullanın.
@@ -77,6 +77,39 @@ Bir güncelleme yayımlamak için:
{/* TODO: add screenshot of the Upgrade button */}
### Sunucu sürümü uyumluluğu
Uygulamanız belirli bir Twenty sunucu sürümünde sunulan bir özelliği kullanıyorsa (örneğin, v2.3.0'da eklenen OAuth sağlayıcıları), uygulamanızın gerektirdiği en düşük sunucu sürümünü `package.json` içindeki `engines.twenty` alanını kullanarak belirtmelisiniz:
```json filename="package.json"
{
"name": "twenty-my-app",
"version": "1.0.0",
"engines": {
"node": "^24.5.0",
"twenty": ">=2.3.0"
}
}
```
Değer, standart bir [semver aralığı](https://github.com/npm/node-semver#ranges)dır. Yaygın Kalıplar:
| Aralık | Anlam |
| ---------------------------------- | --------------------------------------------------------- |
| `>=2.3.0` | 2.3.0'dan itibaren herhangi bir sunucu |
| `>=2.3.0 \<3.0.0` | 2.3.0 veya sonrası, ancak bir sonraki ana sürümün altında |
| `^2.3.0` | `>=2.3.0 \<3.0.0` ile aynı |
**Dağıtım ve yükleme sırasında ne olur:**
* `engines.twenty` ayarlanmışsa ve hedef sunucunun sürümü aralığı karşılamıyorsa, dağıtım (tarball yüklemesi) veya yükleme, gerekli aralığı ve gerçek sunucu sürümünü belirten bir mesajla birlikte `SERVER_VERSION_INCOMPATIBLE` hatasıyla reddedilir.
* `engines.twenty` **ayarlı değilse**, uygulama herhangi bir sunucu sürümünde kabul edilir (mevcut uygulamalarla geriye dönük uyumludur).
* Sunucuda `APP_VERSION` yapılandırılmamışsa, denetim atlanır.
<Note>
Nihai denetim sunucudadır — hem tarball yüklemesinde hem de çalışma alanı (workspace) kurulumunda `engines.twenty`'yi doğrular. Bir tarball'ı bant dışı dağıtırsanız veya marketplace'ten kurarsanız, sunucu yine de uyumluluğu zorunlu kılar.
</Note>
## Otomatik CI/CD (hazır şablonlu iş akışları)
`create-twenty-app` ile oluşturulan uygulamalar, kutudan çıktığı gibi `.github/workflows/` altında iki GitHub Actions iş akışıyla gelir. Depoyu GitHuba iter itmez çalışmaya hazırdır — CI için ek bir kurulum gerekmez ve CD yalnızca tek bir gizli anahtar gerektirir.
@@ -0,0 +1,193 @@
---
title: 连接
description: Let your app act on a user's behalf in third-party services via OAuth.
icon: plug
---
连接是用户为外部服务(Linear、GitHub、Slack 等)持有的凭据。 你的应用声明**如何**获取这些凭据——即**连接提供程序**——并在运行时使用它们向第三方 API 发起认证调用。
目前仅支持 OAuth 2.0。 将来的凭据类型(个人访问令牌、API 密钥、基本身份验证)将接入相同的接口——已经使用 `defineConnectionProvider({ type: 'oauth', ... })` 的应用将无需迁移。
<AccordionGroup>
<Accordion title="defineConnectionProvider" description="声明你的应用如何获取连接">
连接提供程序描述了你的应用所需的 OAuth 握手流程。 用户在你的应用设置中点击"添加连接",完成提供方的授权同意页面后,会在其工作区中创建一条 `ConnectedAccount` 行。
一个可用的配置需要**两个文件**——连接提供程序,以及在 `defineApplication` 上与之匹配、用于保存 OAuth 客户端凭据的 `serverVariables` 声明。
```ts src/connection-providers/linear-connection.ts
import { defineConnectionProvider } from 'twenty-sdk/define';
export default defineConnectionProvider({
universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f',
name: 'linear',
displayName: 'Linear',
icon: 'IconBrandLinear',
type: 'oauth',
oauth: {
authorizationEndpoint: 'https://linear.app/oauth/authorize',
tokenEndpoint: 'https://api.linear.app/oauth/token',
scopes: ['read', 'write'],
// These must match keys in `defineApplication.serverVariables` below.
clientIdVariable: 'LINEAR_CLIENT_ID',
clientSecretVariable: 'LINEAR_CLIENT_SECRET',
// Optional: defaults to 'json'. Some providers (Linear, Slack) want
// 'form-urlencoded' for the token request.
tokenRequestContentType: 'form-urlencoded',
// Optional: defaults to true. Disable only if the provider rejects PKCE.
usePkce: false,
// Optional: extra query params on the authorize URL.
// authorizationParams: { prompt: 'consent' },
// Optional: provider's RFC 7009 token revocation endpoint, called on disconnect.
// revokeEndpoint: 'https://example.com/oauth/revoke',
},
});
```
```ts src/application.config.ts
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: '...',
displayName: 'Linear',
description: 'Connect Linear to Twenty.',
defaultRoleUniversalIdentifier: '...',
// OAuth client credentials live on the app registration (one OAuth app per
// Twenty server, configured by the admin) — not per-workspace. Declare them
// as serverVariables so the admin can fill them in once for all installs.
serverVariables: {
LINEAR_CLIENT_ID: {
description: 'OAuth client ID from your Linear OAuth application.',
isSecret: false,
isRequired: true,
},
LINEAR_CLIENT_SECRET: {
description: 'OAuth client secret from your Linear OAuth application.',
isSecret: true,
isRequired: true,
},
},
});
```
关键点:
* `name` 是在 `listConnections({ providerName })` 中使用的唯一标识符字符串(短横线命名(kebab-case),必须匹配 `^[a-z][a-z0-9-]*$`)。
* `displayName` 会显示在每个应用的设置选项卡以及 AI 工具列表中。
* `clientIdVariable` / `clientSecretVariable` 是**名称**,而不是值——它们必须与 `defineApplication.serverVariables` 中声明的键匹配。 实际的 `client_id` 和 `client_secret` 由服务器管理员通过应用注册 UI 输入,绝不会提交到你的仓库。
* 请使用 `serverVariables`(而非 `applicationVariables`)——OAuth 凭据是服务器范围的,并且每个 Twenty 服务器只配置一个 OAuth 应用。
* 在两个 `serverVariables` 都填写之前,每个应用的设置选项卡会显示"需要服务器管理员"的提示,并且"添加连接"按钮将被禁用。
* `type: 'oauth'` 是目前唯一受支持的取值。 该判别器具备前向兼容性:未来的类型(`'pat'`、`'api-key'` 等) 将会与 `oauth` 并列新增子配置块。
你的提供方需要加入白名单的 OAuth 回调 URL 为:
```
https://<your-twenty-server>/apps/oauth/callback
```
</Accordion>
<Accordion title="listConnections / getConnection" description="在逻辑函数中使用连接">
在逻辑函数处理器内,`listConnections({ providerName })` 会返回此应用针对给定提供方的 `ConnectedAccount` 行,并附带已刷新的访问令牌。
```ts src/logic-functions/handlers/create-linear-issue-handler.ts
import { listConnections } from 'twenty-sdk/logic-function';
export const createLinearIssueHandler = async (input: {
teamId?: string;
title?: string;
}) => {
if (!input.teamId || !input.title) {
return { success: false, error: 'teamId and title are required' };
}
const connections = await listConnections({ providerName: 'linear' });
// Workspace-shared credentials win when present; fall back to the first
// user-visibility one. For HTTP-route triggers you typically pick the
// request user's connection via event.userWorkspaceId instead.
const connection =
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
if (!connection) {
return {
success: false,
error:
'Linear is not connected. Open the app settings and click "Add connection".',
};
}
// Use connection.accessToken to call the third-party API.
const response = await fetch('https://api.linear.app/graphql', {
method: 'POST',
headers: {
Authorization: `Bearer ${connection.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`,
}),
});
return { success: response.ok };
};
```
每个连接包含:
| 字段 | 描述 |
| ----------------- | ---------------------------------------------------- |
| `id` | 唯一的行 id;传给 `getConnection(id)` 以重新获取单个连接 |
| `可见性` | `'user'`(仅对单个工作区成员私有)或 `'workspace'`(与所有成员共享) |
| `范围` | 上游提供方授予的 OAuth 权限(不同于 `visibility`——两者不相关) |
| `userWorkspaceId` | 所有者的 userWorkspace id——在 HTTP 路由触发器中用于选择"请求用户的连接"很有用 |
| `accessToken` | 最新的 OAuth 访问令牌(若已过期会自动刷新) |
| `name` / `handle` | 连接的显示名称(在 OAuth 回调时自动生成,用户可重命名) |
| `authFailedAt` | 当最近一次刷新失败时会设置;用户必须重新连接 |
关键点:
* 传入 `{ providerName }` 以按提供方筛选;省略它则可获取此应用在所有提供方上的全部连接。
* 服务器会在返回前透明地刷新访问令牌。 你的处理器始终会拿到可用的令牌(或已设置 `authFailedAt`)。
* `getConnection(id)` 是获取单行记录的对应方法。
</Accordion>
<Accordion title="按用户与工作区共享的可见性" description="用户如何在私有与共享凭据之间进行选择">
当用户点击"添加连接"时,系统会提示其选择可见性:
* **仅限我**——该凭据仅对连接的用户私有。 代表其调用的任何逻辑函数(带有 `isAuthRequired: true` 的 HTTP 路由触发器)都可以看到它;Cron 触发器和数据库事件则不可。
* **工作区共享**——任何工作区成员都可以使用该凭据。 Cron / 数据库触发器也可以使用它,因为它们没有请求用户。
为每个处理器使用合适的类型:
```ts
// HTTP-route trigger — prefer the request user's own connection.
const conn =
connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ??
connections.find((c) => c.visibility === 'workspace');
// Cron trigger — no request user; only shared credentials are sensible.
const conn = connections.find((c) => c.visibility === 'workspace');
```
每个(用户、提供方)允许有多个连接,因此同一用户可以同时拥有"个人 Linear"和"工作 Linear"。
</Accordion>
<Accordion title="一次性提供方设置" description="在第三方服务中注册你的 OAuth 应用">
对于每个连接提供方,服务器管理员需要先在第三方注册一个 OAuth 应用。
1. 前往提供方的开发者设置(例如 https://linear.app/settings/api/applications/new)。
2. 将**Redirect URI** 设置为 `\<SERVER_URL>/apps/oauth/callback`。
3. 复制生成的**Client ID**和**Client Secret**。
4. 以服务器管理员身份在 Twenty 中打开已安装的应用 → 在相应的 `serverVariables` 上设置这些值。
5. 之后,工作区成员可以在每个应用的**连接**部分添加连接。
</Accordion>
</AccordionGroup>
@@ -77,7 +77,6 @@ export default defineApplication({
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
displayName: 'My Twenty App',
description: 'My first Twenty app',
icon: 'IconWorld',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
@@ -88,11 +88,11 @@ yarn twenty dev --verbose
```
<Warning>
开发模式仅适用于以开发模式运行的 Twenty 实例(`NODE_ENV=development`)。 生产实例会拒绝开发同步请求。 使用 `yarn twenty deploy` 部署到生产服务器——详见[发布应用](/l/zh/developers/extend/apps/publishing)。
开发模式仅适用于以开发模式运行的 Twenty 实例(`NODE_ENV=development`)。 生产实例会拒绝开发同步请求。 使用 `yarn twenty deploy`,然后执行 `yarn twenty install`,即可在生产服务器上发布并安装 — `deploy` 会发布到应用注册表,而 `install` 会在指定的工作区中进行安装。 详情请参见[发布应用](/l/zh/developers/extend/apps/publishing)。
</Warning>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="开发模式终端输出" />
<img src="/images/docs/developers/extends/apps/dev.png" alt="开发模式终端输出" />
</div>
#### 使用 `yarn twenty dev --once` 进行一次性同步
@@ -127,13 +127,7 @@ yarn twenty dev --once
点击 **查看已安装的应用** 以查看已安装的应用。 **关于** 选项卡显示当前版本和管理选项:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="已安装的应用 — “关于”选项卡" />
</div>
切换到 **内容** 选项卡,以查看你的应用提供的全部内容——对象、字段、逻辑函数和智能体:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-4.png" alt="已安装的应用 — “内容”选项卡" />
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="已安装的应用" />
</div>
一切就绪! 编辑 `src/` 中的任意文件,更改会被自动检测到。
@@ -213,30 +207,49 @@ npx create-twenty-app@latest my-twenty-app --example postcard
脚手架工具已经为你启动了一个本地 Twenty 服务器。 稍后要进行管理,请使用 `yarn twenty server`
| 命令 | 描述 |
| -------------------------------------- | -------------------- |
| `yarn twenty server start` | 启动本地服务器(按需拉取镜像) |
| `yarn twenty server start --port 3030` | 在自定义端口启动 |
| `yarn twenty server start --test` | 在 2021 端口启动一个独立的测试实例 |
| `yarn twenty server stop` | 停止服务器(保留数据) |
| `yarn twenty server status` | 显示服务器状态、URL 和凭据 |
| `yarn twenty server logs` | 流式输出服务器日志 |
| `yarn twenty server logs --lines 100` | 显示最近 100 行日志 |
| `yarn twenty server reset` | 删除所有数据并全新开始 |
| 命令 | 描述 |
| -------------------------------------- | -------------------------------- |
| `yarn twenty server start` | 启动本地服务器(按需拉取镜像) |
| `yarn twenty server start --port 3030` | 在自定义端口启动 |
| `yarn twenty server start --test` | 在 2021 端口启动一个独立的测试实例 |
| `yarn twenty server stop` | 停止服务器(保留数据) |
| `yarn twenty server status` | 显示服务器状态、URL、版本和凭据 |
| `yarn twenty server logs` | 流式输出服务器日志 |
| `yarn twenty server logs --lines 100` | 显示最近 100 行日志 |
| `yarn twenty server reset` | 删除所有数据并全新开始 |
| `yarn twenty server upgrade` | 拉取最新的 `twenty-app-dev` 镜像并重新创建容器 |
| `yarn twenty server upgrade 2.2.0` | 升级到指定版本 |
数据在重启后会保留,存储于两个 Docker 卷中(`twenty-app-dev-data` 用于 PostgreSQL`twenty-app-dev-storage` 用于文件)。 使用 `reset` 清空所有内容并重新开始。
### 升级服务器镜像
使用 `yarn twenty server upgrade` 来检查是否有更新的 `twenty-app-dev` Docker 镜像并更新容器。 该命令会拉取镜像,与容器创建时所用的镜像进行比较,仅当镜像确实发生变化时才会重新创建容器。 您的数据卷将被保留——只会替换容器。
```bash filename="Terminal"
# Upgrade to the latest version (skips recreation if already up to date)
yarn twenty server upgrade
# Upgrade to a specific version
yarn twenty server upgrade 2.2.0
```
如果有可用的新镜像且容器正在运行,升级命令会使用更新后的镜像自动启动一个新容器。 随后运行 `yarn twenty server start`,等待其变为健康状态。 如果镜像没有变化,容器将保持不变。
您可以使用 `yarn twenty server status` 验证正在运行的版本,该命令会显示容器中的 `APP_VERSION`。
### 运行测试实例
向任何 `server` 命令传递 `--test` 以管理第二个、完全隔离的实例——这对于运行集成测试或在不影响主开发数据的情况下进行试验非常有用。
| 命令 | 描述 |
| ---------------------------------- | ------------------- |
| `yarn twenty server start --test` | 启动测试实例 (默认端口为 2021) |
| `yarn twenty server stop --test` | 停止测试实例 |
| `yarn twenty server status --test` | 显示测试实例状态、URL 和凭据 |
| `yarn twenty server logs --test` | 流式输出测试实例日志 |
| `yarn twenty server reset --test` | 清除测试数据并全新开始 |
| 命令 | 描述 |
| ----------------------------------- | ------------------- |
| `yarn twenty server start --test` | 启动测试实例 (默认端口为 2021) |
| `yarn twenty server stop --test` | 停止测试实例 |
| `yarn twenty server status --test` | 显示测试实例状态、URL、版本和凭据 |
| `yarn twenty server logs --test` | 流式输出测试实例日志 |
| `yarn twenty server reset --test` | 清除测试数据并全新开始 |
| `yarn twenty server upgrade --test` | 升级测试实例镜像 |
测试实例在其独立的 Docker 容器 (`twenty-app-dev-test`) 中运行,配有专用的卷 (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`) 和配置,因此可以与主实例并行运行且不会发生冲突。 将 `--test` 与 `--port` 一起使用以覆盖默认的 2021 端口。
@@ -77,6 +77,39 @@ yarn twenty deploy
{/* TODO: add screenshot of the Upgrade button */}
### 服务器版本兼容性
如果你的应用使用了特定 Twenty 服务器版本中引入的功能(例如在 v2.3.0 中新增的 OAuth 提供方),应当在 `package.json` 的 `engines.twenty` 字段中声明应用所需的最低服务器版本:
```json filename="package.json"
{
"name": "twenty-my-app",
"version": "1.0.0",
"engines": {
"node": "^24.5.0",
"twenty": ">=2.3.0"
}
}
```
该值是标准的 [semver 范围](https://github.com/npm/node-semver#ranges)。 常见模式:
| 范围 | 含义 |
| ---------------------------------- | --------------------------------------- |
| `>=2.3.0` | 任何 2.3.0 及以上的服务器 |
| `>=2.3.0 \<3.0.0` | 2.3.0 或更高,但低于下一个主版本 |
| `^2.3.0` | 与 `>=2.3.0 \<3.0.0` 相同 |
**在部署和安装时会发生什么:**
* 如果已设置 `engines.twenty`,且目标服务器的版本不满足该范围,则部署(tarball 上传)或安装将被拒绝,并返回 `SERVER_VERSION_INCOMPATIBLE` 错误以及一条同时指明所需范围和实际服务器版本的消息。
* 如果 `engines.twenty` **未设置**,则该应用可在任何服务器版本上被接受(与现有应用向后兼容)。
* 如果服务器未配置 `APP_VERSION`,则跳过该检查。
<Note>
服务器是权威校验方——它会在 tarball 上传和工作区安装时验证 `engines.twenty`。 即使你通过带外方式部署 tarball 或从应用市场安装,服务器仍会强制执行兼容性要求。
</Note>
## 自动化 CI/CD(脚手架生成的工作流)
使用 `create-twenty-app` 生成的应用开箱即带有两个 GitHub Actions 工作流,位于 `.github/workflows/`。 当你将仓库推送到 GitHub 后即可运行——CI 无需额外设置,CD 只需要一个机密。
+4
View File
@@ -11,6 +11,7 @@
"dependencies": {
"@lingui/core": "^5.1.2",
"@lingui/react": "^5.1.2",
"@react-email/components": "^0.5.3",
"twenty-shared": "workspace:*"
},
"peerDependencies": {
@@ -25,7 +26,10 @@
"@tiptap/core": "^3.4.2",
"@types/react": "^19",
"@types/react-dom": "^19",
"@typescript/native-preview": "^7.0.0-dev.20260116.1",
"@vitejs/plugin-react-swc": "4.2.3",
"react-email": "5.1.0",
"tsc-alias": "^1.8.16",
"vite-plugin-dts": "3.8.1"
},
"exports": {
@@ -38,11 +38,14 @@
"@types/node": "^24.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@typescript/native-preview": "^7.0.0-dev.20260116.1",
"@vitest/browser-playwright": "^4.0.18",
"playwright": "^1.56.1",
"prettier": "^3.1.1",
"storybook": "^10.2.13",
"styled-components": "^6.1.0",
"ts-morph": "^25.0.0",
"tsc-alias": "^1.8.16",
"tsx": "^4.7.0",
"twenty-sdk": "workspace:*",
"twenty-shared": "workspace:*",
+2 -2
View File
@@ -61,8 +61,8 @@ const jestConfig = {
extensionsToTreatAsEsm: ['.ts', '.tsx'],
coverageThreshold: {
global: {
statements: 47.9,
lines: 46,
statements: 47.3,
lines: 45.9,
functions: 39.5,
},
},
+65 -2
View File
@@ -57,7 +57,8 @@
"@react-email/components": "^0.5.3",
"@react-pdf/renderer": "^4.1.6",
"@scalar/api-reference-react": "^0.4.36",
"@sentry/react": "^10.27.0",
"@sentry/react": "^10.51.0",
"@sniptt/guards": "^0.2.0",
"@tiptap/core": "3.4.2",
"@tiptap/extension-bold": "3.4.2",
"@tiptap/extension-document": "3.4.2",
@@ -79,24 +80,43 @@
"apollo-link-rest": "^0.10.0-rc.2",
"apollo-upload-client": "^19.0.0",
"buffer": "^6.0.3",
"country-flag-icons": "^1.5.11",
"cron-parser": "5.1.1",
"d3-shape": "^3.2.0",
"date-fns": "^2.30.0",
"date-fns-tz": "^2.0.0",
"deep-equal": "^2.2.2",
"file-saver": "^2.0.5",
"framer-motion": "^11.18.0",
"fuse.js": "^7.1.0",
"graphiql": "^3.1.1",
"graphql": "16.8.1",
"graphql-sse": "^2.5.4",
"graphql-tag": "^2.12.6",
"immer": "^10.1.1",
"input-otp": "^1.4.2",
"jotai": "^2.17.1",
"js-cookie": "^3.0.5",
"json-2-csv": "^5.4.0",
"json-logic-js": "^2.0.5",
"jwt-decode": "^4.0.0",
"libphonenumber-js": "^1.10.26",
"linkify-react": "^4.1.3",
"linkifyjs": "^4.1.3",
"lodash.camelcase": "^4.3.0",
"lodash.groupby": "^4.6.0",
"lodash.isempty": "^4.4.0",
"lodash.omit": "^4.5.0",
"lodash.uniqby": "^4.7.0",
"marked": "^17.0.1",
"microdiff": "^1.3.2",
"papaparse": "^5.4.1",
"pluralize": "^8.0.0",
"qs": "^6.11.2",
"react": "^18.2.0",
"react-data-grid": "7.0.0-beta.13",
"react-datepicker": "^6.7.1",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-error-boundary": "^4.0.11",
"react-grid-layout": "^1.5.2",
@@ -113,29 +133,72 @@
"react-router-dom": "^6.4.4",
"react-textarea-autosize": "^8.4.1",
"remark-gfm": "^4.0.1",
"rxjs": "^7.2.0",
"temporal-polyfill": "^0.3.0",
"transliteration": "^2.3.5",
"ts-key-enum": "^2.0.12",
"twenty-front-component-renderer": "workspace:*",
"twenty-shared": "workspace:*",
"twenty-ui": "workspace:*",
"use-debounce": "^10.0.0"
"type-fest": "4.10.1",
"use-debounce": "^10.0.0",
"uuid": "^9.0.0",
"xlsx-ugnis": "^0.19.3",
"zod": "^4.1.11"
},
"devDependencies": {
"@babel/core": "^7.14.5",
"@babel/preset-typescript": "^7.24.6",
"@chromatic-com/storybook": "^4.1.3",
"@graphql-codegen/cli": "^3.3.1",
"@graphql-codegen/typed-document-node": "^5.0.9",
"@graphql-codegen/typescript": "^3.0.4",
"@graphql-codegen/typescript-operations": "^3.0.4",
"@lingui/cli": "^5.1.2",
"@lingui/swc-plugin": "^5.11.0",
"@lingui/vite-plugin": "^5.1.2",
"@playwright/test": "^1.56.1",
"@storybook-community/storybook-addon-cookie": "^5.0.0",
"@storybook/addon-coverage": "^3.0.0",
"@storybook/addon-docs": "^10.3.3",
"@storybook/addon-links": "^10.3.3",
"@storybook/react-vite": "^10.3.3",
"@swc/core": "^1.15.11",
"@swc/jest": "^0.2.39",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@tiptap/suggestion": "3.4.2",
"@types/deep-equal": "^1.0.1",
"@types/file-saver": "^2.0.7",
"@types/jest": "^30.0.0",
"@types/js-cookie": "^3.0.3",
"@types/json-logic-js": "^2",
"@types/react-datepicker": "^6.2.0",
"@types/react-grid-layout": "^1",
"@types/uuid": "^9.0.2",
"@vitejs/plugin-react-swc": "4.2.3",
"@vitest/coverage-istanbul": "^4.0.18",
"@wyw-in-js/vite": "^0.7.0",
"chromatic": "^6.18.0",
"dotenv-cli": "^7.4.4",
"jest": "29.7.0",
"jest-environment-jsdom": "30.0.0-beta.3",
"jest-fetch-mock": "^3.0.3",
"monaco-editor": "^0.51.0",
"monaco-editor-auto-typings": "^0.4.5",
"msw": "^2.12.7",
"msw-storybook-addon": "^2.0.6",
"optionator": "^0.9.1",
"oxlint": "^1.51.0",
"oxlint-tsgolint": "^0.16.0",
"playwright": "^1.56.1",
"prettier": "^3.1.1",
"rollup-plugin-node-polyfills": "^0.2.1",
"rollup-plugin-visualizer": "^5.14.0",
"storybook-addon-mock-date": "2.0.0",
"storybook-addon-pseudo-states": "^10.3.3",
"ts-jest": "^29.1.1",
"vite-plugin-svgr": "^4.3.0",
"vite-tsconfig-paths": "^4.2.1"
}
@@ -291,6 +291,21 @@ export enum HealthIndicatorId {
worker = 'worker'
}
export type InstanceAndAllWorkspacesUpgradeStatus = {
__typename?: 'InstanceAndAllWorkspacesUpgradeStatus';
computedAt: Scalars['DateTime'];
instanceUpgradeStatus: InstanceUpgradeStatus;
workspacesBehind: Array<WorkspaceUpgradeRef>;
workspacesFailed: Array<WorkspaceUpgradeRef>;
};
export type InstanceUpgradeStatus = {
__typename?: 'InstanceUpgradeStatus';
health: UpgradeHealth;
inferredVersion?: Maybe<Scalars['String']>;
latestCommand?: Maybe<LatestUpgradeCommand>;
};
export type JobOperationResult = {
__typename?: 'JobOperationResult';
error?: Maybe<Scalars['String']>;
@@ -309,6 +324,15 @@ export enum JobState {
WAITING_CHILDREN = 'WAITING_CHILDREN'
}
export type LatestUpgradeCommand = {
__typename?: 'LatestUpgradeCommand';
createdAt: Scalars['DateTime'];
errorMessage?: Maybe<Scalars['String']>;
executedByVersion: Scalars['String'];
name: Scalars['String'];
status: Scalars['String'];
};
export type MaintenanceMode = {
__typename?: 'MaintenanceMode';
endAt: Scalars['DateTime'];
@@ -353,6 +377,7 @@ export type Mutation = {
createDatabaseConfigVariable: Scalars['Boolean'];
deleteDatabaseConfigVariable: Scalars['Boolean'];
deleteJobs: DeleteJobsResponse;
refreshUpgradeStatus: InstanceAndAllWorkspacesUpgradeStatus;
removeAiProvider: Scalars['Boolean'];
removeModelFromProvider: Scalars['Boolean'];
retryJobs: RetryJobsResponse;
@@ -476,12 +501,14 @@ export type Query = {
getConfigVariablesGrouped: ConfigVariables;
getDatabaseConfigVariable: ConfigVariable;
getIndicatorHealthStatus: AdminPanelHealthServiceData;
getInstanceAndAllWorkspacesUpgradeStatus: InstanceAndAllWorkspacesUpgradeStatus;
getMaintenanceMode?: Maybe<MaintenanceMode>;
getModelsDevProviders: Array<ModelsDevProviderSuggestion>;
getModelsDevSuggestions: Array<ModelsDevModelSuggestion>;
getQueueJobs: QueueJobsResponse;
getQueueMetrics: QueueMetricsData;
getSystemHealthStatus: SystemHealth;
getUpgradeStatus: Array<WorkspaceUpgradeStatus>;
userLookupAdminPanel: UserLookup;
versionInfo: VersionInfo;
workspaceBillingAdminPanel?: Maybe<AdminPanelWorkspaceBilling>;
@@ -549,6 +576,11 @@ export type QueryGetQueueMetricsArgs = {
};
export type QueryGetUpgradeStatusArgs = {
workspaceIds: Array<Scalars['UUID']>;
};
export type QueryUserLookupAdminPanelArgs = {
userIdentifier: Scalars['String'];
};
@@ -659,6 +691,12 @@ export type SystemHealthService = {
status: AdminPanelHealthServiceStatus;
};
export enum UpgradeHealth {
BEHIND = 'BEHIND',
FAILED = 'FAILED',
UP_TO_DATE = 'UP_TO_DATE'
}
export type UsageBreakdownItem = {
__typename?: 'UsageBreakdownItem';
creditsUsed: Scalars['Float'];
@@ -678,7 +716,7 @@ export type UserInfo = {
export type UserLookup = {
__typename?: 'UserLookup';
user: UserInfo;
user?: Maybe<UserInfo>;
workspaces: Array<WorkspaceInfo>;
};
@@ -722,6 +760,21 @@ export type WorkspaceInfo = {
workspaceUrls: WorkspaceUrls;
};
export type WorkspaceUpgradeRef = {
__typename?: 'WorkspaceUpgradeRef';
id: Scalars['UUID'];
name?: Maybe<Scalars['String']>;
};
export type WorkspaceUpgradeStatus = {
__typename?: 'WorkspaceUpgradeStatus';
displayName?: Maybe<Scalars['String']>;
health: UpgradeHealth;
inferredVersion?: Maybe<Scalars['String']>;
latestCommand?: Maybe<LatestUpgradeCommand>;
workspaceId: Scalars['UUID'];
};
export type WorkspaceUrls = {
__typename?: 'WorkspaceUrls';
customUrl?: Maybe<Scalars['String']>;
@@ -915,6 +968,13 @@ export type GetAdminWorkspaceChatThreadsQueryVariables = Exact<{
export type GetAdminWorkspaceChatThreadsQuery = { __typename?: 'Query', getAdminWorkspaceChatThreads: Array<{ __typename?: 'AdminWorkspaceChatThread', id: string, title?: string | null, totalInputTokens: number, totalOutputTokens: number, conversationSize: number, createdAt: string, updatedAt: string }> };
export type GetUpgradeStatusQueryVariables = Exact<{
workspaceIds: Array<Scalars['UUID']> | Scalars['UUID'];
}>;
export type GetUpgradeStatusQuery = { __typename?: 'Query', getUpgradeStatus: Array<{ __typename?: 'WorkspaceUpgradeStatus', workspaceId: string, displayName?: string | null, inferredVersion?: string | null, health: UpgradeHealth, latestCommand?: { __typename?: 'LatestUpgradeCommand', name: string, status: string, executedByVersion: string, errorMessage?: string | null, createdAt: string } | null }> };
export type GetVersionInfoQueryVariables = Exact<{ [key: string]: never; }>;
@@ -932,14 +992,14 @@ export type UserLookupAdminPanelQueryVariables = Exact<{
}>;
export type UserLookupAdminPanelQuery = { __typename?: 'Query', userLookupAdminPanel: { __typename?: 'UserLookup', user: { __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null, createdAt: string }, workspaces: Array<{ __typename?: 'WorkspaceInfo', id: string, name: string, logo?: string | null, totalUsers: number, activationStatus: WorkspaceActivationStatus, createdAt: string, allowImpersonation: boolean, workspaceUrls: { __typename?: 'WorkspaceUrls', customUrl?: string | null, subdomainUrl: string }, users: Array<{ __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }>, featureFlags: Array<{ __typename?: 'FeatureFlag', key: FeatureFlagKey, value: boolean }> }> } };
export type UserLookupAdminPanelQuery = { __typename?: 'Query', userLookupAdminPanel: { __typename?: 'UserLookup', user?: { __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null, createdAt: string } | null, workspaces: Array<{ __typename?: 'WorkspaceInfo', id: string, name: string, logo?: string | null, totalUsers: number, activationStatus: WorkspaceActivationStatus, createdAt: string, allowImpersonation: boolean, workspaceUrls: { __typename?: 'WorkspaceUrls', customUrl?: string | null, subdomainUrl: string }, users: Array<{ __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }>, featureFlags: Array<{ __typename?: 'FeatureFlag', key: FeatureFlagKey, value: boolean }> }> } };
export type WorkspaceLookupAdminPanelQueryVariables = Exact<{
workspaceId: Scalars['UUID'];
}>;
export type WorkspaceLookupAdminPanelQuery = { __typename?: 'Query', workspaceLookupAdminPanel: { __typename?: 'UserLookup', user: { __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null, createdAt: string }, workspaces: Array<{ __typename?: 'WorkspaceInfo', id: string, name: string, allowImpersonation: boolean, logo?: string | null, totalUsers: number, activationStatus: WorkspaceActivationStatus, createdAt: string, workspaceUrls: { __typename?: 'WorkspaceUrls', customUrl?: string | null, subdomainUrl: string }, users: Array<{ __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null, avatarUrl?: string | null }>, featureFlags: Array<{ __typename?: 'FeatureFlag', key: FeatureFlagKey, value: boolean }> }> } };
export type WorkspaceLookupAdminPanelQuery = { __typename?: 'Query', workspaceLookupAdminPanel: { __typename?: 'UserLookup', user?: { __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null, createdAt: string } | null, workspaces: Array<{ __typename?: 'WorkspaceInfo', id: string, name: string, allowImpersonation: boolean, logo?: string | null, totalUsers: number, activationStatus: WorkspaceActivationStatus, createdAt: string, workspaceUrls: { __typename?: 'WorkspaceUrls', customUrl?: string | null, subdomainUrl: string }, users: Array<{ __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null, avatarUrl?: string | null }>, featureFlags: Array<{ __typename?: 'FeatureFlag', key: FeatureFlagKey, value: boolean }> }> } };
export type DeleteJobsMutationVariables = Exact<{
queueName: Scalars['String'];
@@ -949,6 +1009,11 @@ export type DeleteJobsMutationVariables = Exact<{
export type DeleteJobsMutation = { __typename?: 'Mutation', deleteJobs: { __typename?: 'DeleteJobsResponse', deletedCount: number, results: Array<{ __typename?: 'JobOperationResult', jobId: string, success: boolean, error?: string | null }> } };
export type RefreshUpgradeStatusMutationVariables = Exact<{ [key: string]: never; }>;
export type RefreshUpgradeStatusMutation = { __typename?: 'Mutation', refreshUpgradeStatus: { __typename?: 'InstanceAndAllWorkspacesUpgradeStatus', computedAt: string, instanceUpgradeStatus: { __typename?: 'InstanceUpgradeStatus', inferredVersion?: string | null, health: UpgradeHealth, latestCommand?: { __typename?: 'LatestUpgradeCommand', name: string, status: string, executedByVersion: string, errorMessage?: string | null, createdAt: string } | null }, workspacesBehind: Array<{ __typename?: 'WorkspaceUpgradeRef', id: string, name?: string | null }>, workspacesFailed: Array<{ __typename?: 'WorkspaceUpgradeRef', id: string, name?: string | null }> } };
export type RetryJobsMutationVariables = Exact<{
queueName: Scalars['String'];
jobIds: Array<Scalars['String']> | Scalars['String'];
@@ -964,6 +1029,11 @@ export type GetIndicatorHealthStatusQueryVariables = Exact<{
export type GetIndicatorHealthStatusQuery = { __typename?: 'Query', getIndicatorHealthStatus: { __typename?: 'AdminPanelHealthServiceData', id: HealthIndicatorId, label: string, description: string, status: AdminPanelHealthServiceStatus, errorMessage?: string | null, details?: string | null, queues?: Array<{ __typename?: 'AdminPanelWorkerQueueHealth', id: string, queueName: string, status: AdminPanelHealthServiceStatus }> | null } };
export type GetInstanceAndAllWorkspacesUpgradeStatusQueryVariables = Exact<{ [key: string]: never; }>;
export type GetInstanceAndAllWorkspacesUpgradeStatusQuery = { __typename?: 'Query', getInstanceAndAllWorkspacesUpgradeStatus: { __typename?: 'InstanceAndAllWorkspacesUpgradeStatus', computedAt: string, instanceUpgradeStatus: { __typename?: 'InstanceUpgradeStatus', inferredVersion?: string | null, health: UpgradeHealth, latestCommand?: { __typename?: 'LatestUpgradeCommand', name: string, status: string, executedByVersion: string, errorMessage?: string | null, createdAt: string } | null }, workspacesBehind: Array<{ __typename?: 'WorkspaceUpgradeRef', id: string, name?: string | null }>, workspacesFailed: Array<{ __typename?: 'WorkspaceUpgradeRef', id: string, name?: string | null }> } };
export type GetQueueJobsQueryVariables = Exact<{
queueName: Scalars['String'];
state: JobState;
@@ -1036,13 +1106,16 @@ export const AdminPanelTopWorkspacesDocument = {"kind":"Document","definitions":
export const FindOneAdminApplicationRegistrationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindOneAdminApplicationRegistration"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findOneAdminApplicationRegistration"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ApplicationRegistrationFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ApplicationRegistrationFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ApplicationRegistration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"logoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthClientId"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthRedirectUris"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthScopes"}},{"kind":"Field","name":{"kind":"Name","value":"sourceType"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePackage"}},{"kind":"Field","name":{"kind":"Name","value":"latestAvailableVersion"}},{"kind":"Field","name":{"kind":"Name","value":"isListed"}},{"kind":"Field","name":{"kind":"Name","value":"isFeatured"}},{"kind":"Field","name":{"kind":"Name","value":"isPreInstalled"}},{"kind":"Field","name":{"kind":"Name","value":"ownerWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<FindOneAdminApplicationRegistrationQuery, FindOneAdminApplicationRegistrationQueryVariables>;
export const GetAdminChatThreadMessagesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAdminChatThreadMessages"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getAdminChatThreadMessages"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"threadId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"thread"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"totalInputTokens"}},{"kind":"Field","name":{"kind":"Name","value":"totalOutputTokens"}},{"kind":"Field","name":{"kind":"Name","value":"conversationSize"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"messages"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"parts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"textContent"}},{"kind":"Field","name":{"kind":"Name","value":"toolName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode<GetAdminChatThreadMessagesQuery, GetAdminChatThreadMessagesQueryVariables>;
export const GetAdminWorkspaceChatThreadsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAdminWorkspaceChatThreads"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getAdminWorkspaceChatThreads"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"totalInputTokens"}},{"kind":"Field","name":{"kind":"Name","value":"totalOutputTokens"}},{"kind":"Field","name":{"kind":"Name","value":"conversationSize"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<GetAdminWorkspaceChatThreadsQuery, GetAdminWorkspaceChatThreadsQueryVariables>;
export const GetUpgradeStatusDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetUpgradeStatus"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceIds"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getUpgradeStatus"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceIds"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"inferredVersion"}},{"kind":"Field","name":{"kind":"Name","value":"health"}},{"kind":"Field","name":{"kind":"Name","value":"latestCommand"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"executedByVersion"}},{"kind":"Field","name":{"kind":"Name","value":"errorMessage"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode<GetUpgradeStatusQuery, GetUpgradeStatusQueryVariables>;
export const GetVersionInfoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetVersionInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"versionInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"currentVersion"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}}]}}]}}]} as unknown as DocumentNode<GetVersionInfoQuery, GetVersionInfoQueryVariables>;
export const WorkspaceBillingAdminPanelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"WorkspaceBillingAdminPanel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceBillingAdminPanel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stripeCustomerId"}},{"kind":"Field","name":{"kind":"Name","value":"creditBalance"}},{"kind":"Field","name":{"kind":"Name","value":"subscription"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stripeSubscriptionId"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"interval"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}},{"kind":"Field","name":{"kind":"Name","value":"planKey"}},{"kind":"Field","name":{"kind":"Name","value":"currentPeriodStart"}},{"kind":"Field","name":{"kind":"Name","value":"currentPeriodEnd"}},{"kind":"Field","name":{"kind":"Name","value":"trialStart"}},{"kind":"Field","name":{"kind":"Name","value":"trialEnd"}},{"kind":"Field","name":{"kind":"Name","value":"cancelAt"}},{"kind":"Field","name":{"kind":"Name","value":"canceledAt"}},{"kind":"Field","name":{"kind":"Name","value":"cancelAtPeriodEnd"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"productName"}},{"kind":"Field","name":{"kind":"Name","value":"productKey"}},{"kind":"Field","name":{"kind":"Name","value":"stripePriceId"}},{"kind":"Field","name":{"kind":"Name","value":"quantity"}},{"kind":"Field","name":{"kind":"Name","value":"unitAmount"}},{"kind":"Field","name":{"kind":"Name","value":"includedCredits"}}]}}]}}]}}]}}]} as unknown as DocumentNode<WorkspaceBillingAdminPanelQuery, WorkspaceBillingAdminPanelQueryVariables>;
export const UserLookupAdminPanelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserLookupAdminPanel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userIdentifier"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userLookupAdminPanel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"userIdentifier"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userIdentifier"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"UserInfoFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"totalUsers"}},{"kind":"Field","name":{"kind":"Name","value":"activationStatus"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"allowImpersonation"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceUrls"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"customUrl"}},{"kind":"Field","name":{"kind":"Name","value":"subdomainUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"users"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"featureFlags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UserInfoFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"UserInfo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode<UserLookupAdminPanelQuery, UserLookupAdminPanelQueryVariables>;
export const WorkspaceLookupAdminPanelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"WorkspaceLookupAdminPanel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceLookupAdminPanel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"UserInfoFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"allowImpersonation"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"totalUsers"}},{"kind":"Field","name":{"kind":"Name","value":"activationStatus"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceUrls"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"customUrl"}},{"kind":"Field","name":{"kind":"Name","value":"subdomainUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"users"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"featureFlags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UserInfoFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"UserInfo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode<WorkspaceLookupAdminPanelQuery, WorkspaceLookupAdminPanelQueryVariables>;
export const DeleteJobsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteJobs"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"queueName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"jobIds"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteJobs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"queueName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"queueName"}}},{"kind":"Argument","name":{"kind":"Name","value":"jobIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"jobIds"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deletedCount"}},{"kind":"Field","name":{"kind":"Name","value":"results"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"jobId"}},{"kind":"Field","name":{"kind":"Name","value":"success"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]}}]} as unknown as DocumentNode<DeleteJobsMutation, DeleteJobsMutationVariables>;
export const RefreshUpgradeStatusDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RefreshUpgradeStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"refreshUpgradeStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"instanceUpgradeStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"inferredVersion"}},{"kind":"Field","name":{"kind":"Name","value":"health"}},{"kind":"Field","name":{"kind":"Name","value":"latestCommand"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"executedByVersion"}},{"kind":"Field","name":{"kind":"Name","value":"errorMessage"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspacesBehind"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspacesFailed"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"computedAt"}}]}}]}}]} as unknown as DocumentNode<RefreshUpgradeStatusMutation, RefreshUpgradeStatusMutationVariables>;
export const RetryJobsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RetryJobs"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"queueName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"jobIds"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"retryJobs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"queueName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"queueName"}}},{"kind":"Argument","name":{"kind":"Name","value":"jobIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"jobIds"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"retriedCount"}},{"kind":"Field","name":{"kind":"Name","value":"results"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"jobId"}},{"kind":"Field","name":{"kind":"Name","value":"success"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]}}]} as unknown as DocumentNode<RetryJobsMutation, RetryJobsMutationVariables>;
export const GetIndicatorHealthStatusDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetIndicatorHealthStatus"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"indicatorId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"HealthIndicatorId"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getIndicatorHealthStatus"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"indicatorId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"indicatorId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"errorMessage"}},{"kind":"Field","name":{"kind":"Name","value":"details"}},{"kind":"Field","name":{"kind":"Name","value":"queues"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"queueName"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]}}]} as unknown as DocumentNode<GetIndicatorHealthStatusQuery, GetIndicatorHealthStatusQueryVariables>;
export const GetInstanceAndAllWorkspacesUpgradeStatusDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetInstanceAndAllWorkspacesUpgradeStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getInstanceAndAllWorkspacesUpgradeStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"instanceUpgradeStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"inferredVersion"}},{"kind":"Field","name":{"kind":"Name","value":"health"}},{"kind":"Field","name":{"kind":"Name","value":"latestCommand"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"executedByVersion"}},{"kind":"Field","name":{"kind":"Name","value":"errorMessage"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspacesBehind"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspacesFailed"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"computedAt"}}]}}]}}]} as unknown as DocumentNode<GetInstanceAndAllWorkspacesUpgradeStatusQuery, GetInstanceAndAllWorkspacesUpgradeStatusQueryVariables>;
export const GetQueueJobsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetQueueJobs"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"queueName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"state"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"JobState"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"offset"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getQueueJobs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"queueName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"queueName"}}},{"kind":"Argument","name":{"kind":"Name","value":"state"},"value":{"kind":"Variable","name":{"kind":"Name","value":"state"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"Argument","name":{"kind":"Name","value":"offset"},"value":{"kind":"Variable","name":{"kind":"Name","value":"offset"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"jobs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"data"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"failedReason"}},{"kind":"Field","name":{"kind":"Name","value":"processedOn"}},{"kind":"Field","name":{"kind":"Name","value":"finishedOn"}},{"kind":"Field","name":{"kind":"Name","value":"attemptsMade"}},{"kind":"Field","name":{"kind":"Name","value":"returnValue"}},{"kind":"Field","name":{"kind":"Name","value":"logs"}},{"kind":"Field","name":{"kind":"Name","value":"stackTrace"}}]}},{"kind":"Field","name":{"kind":"Name","value":"count"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"hasMore"}},{"kind":"Field","name":{"kind":"Name","value":"retentionConfig"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"completedMaxAge"}},{"kind":"Field","name":{"kind":"Name","value":"completedMaxCount"}},{"kind":"Field","name":{"kind":"Name","value":"failedMaxAge"}},{"kind":"Field","name":{"kind":"Name","value":"failedMaxCount"}}]}}]}}]}}]} as unknown as DocumentNode<GetQueueJobsQuery, GetQueueJobsQueryVariables>;
export const GetQueueMetricsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetQueueMetrics"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"queueName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"timeRange"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"QueueMetricsTimeRange"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getQueueMetrics"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"queueName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"queueName"}}},{"kind":"Argument","name":{"kind":"Name","value":"timeRange"},"value":{"kind":"Variable","name":{"kind":"Name","value":"timeRange"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"queueName"}},{"kind":"Field","name":{"kind":"Name","value":"timeRange"}},{"kind":"Field","name":{"kind":"Name","value":"workers"}},{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"failed"}},{"kind":"Field","name":{"kind":"Name","value":"completed"}},{"kind":"Field","name":{"kind":"Name","value":"waiting"}},{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"delayed"}},{"kind":"Field","name":{"kind":"Name","value":"failureRate"}}]}},{"kind":"Field","name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"x"}},{"kind":"Field","name":{"kind":"Name","value":"y"}}]}}]}}]}}]}}]} as unknown as DocumentNode<GetQueueMetricsQuery, GetQueueMetricsQueryVariables>;
export const GetSystemHealthStatusDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSystemHealthStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getSystemHealthStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"services"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]}}]} as unknown as DocumentNode<GetSystemHealthStatusQuery, GetSystemHealthStatusQueryVariables>;
File diff suppressed because one or more lines are too long
@@ -1,4 +1,5 @@
import { useOpenSettingsMenu } from '@/navigation/hooks/useOpenSettings';
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { type SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
@@ -7,19 +8,22 @@ export const useNavigateSettings = () => {
const navigate = useNavigate();
const { openSettingsMenu } = useOpenSettingsMenu();
return <T extends SettingsPath>(
to: T,
params?: Parameters<typeof getSettingsPath<T>>[1],
queryParams?: Record<string, any>,
options?: {
replace?: boolean;
state?: any;
},
hash?: string,
) => {
openSettingsMenu();
return useCallback(
<T extends SettingsPath>(
to: T,
params?: Parameters<typeof getSettingsPath<T>>[1],
queryParams?: Record<string, any>,
options?: {
replace?: boolean;
state?: any;
},
hash?: string,
) => {
openSettingsMenu();
const path = getSettingsPath(to, params, queryParams, hash);
return navigate(path, options);
};
const path = getSettingsPath(to, params, queryParams, hash);
return navigate(path, options);
},
[navigate, openSettingsMenu],
);
};
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long

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