6a2c948e848092783da60c1909dbb8f9b64fd1a8
139 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
15eaabdbc1 |
fix(ai) - optimize crud tools (#21133)
- **Add delete many**, `delete_many_{object}` added alongside the
existing `delete_one_{object}`.
- **Uniformize naming**, crud module, type names, and MCP helper
constants renamed for consistency.
- **Optimize tool schema (learn phase)**
- `find_many(_companies)`: **7 158 → 2 700 tokens**
- `find_one(_company)`: **280 → 126 tokens**
- ....
- Main mechanism: `reused: 'ref'` (line 7 of
`to-tool-json-schema.util.ts`). Zod walks the schema tree, tracks which
Zod schema instances appear more than once, and emits each reused
instance exactly once in `$defs`, replacing all subsequent occurrences
with a `$ref`. Works because filter and value schemas are now extracted
as shared objects.
- **Optimize system prompt (tool catalog)**, DATABASE_CRUD section
restructured to list operation patterns (`find_many_{object}`, …) once +
objects once, instead of the full N×M cross-product of tool names.
- **Optimize execute_tool**, shared record-properties schema (same
`$defs` deduplication applies at call time); introduced `upsert_many`;
added `selectedFields` to `find_*` so the agent only fetches the fields
it needs.
|
||
|
|
c2ad3f3614 |
(partners): bump app version 0.3.3 -> 0.3.4 (#21167)
Forgot to bump the version when merging #21162 from `0.3.3` to `0.3.4` |
||
|
|
ff5d082e7c |
feat(partners): remove Project Budget Typical field, rework partner views & nav order (#21162)
## Summary Partners-app changes spanning the Partner object, its data scripts, table views, and sidebar navigation. ### Remove the "Project Budget Typical" field Dropped the `projectBudgetTypical` currency field from the Partner object and every reference to it: - `get-partner-by-slug` and `list-available-partners` logic-function selections - the seed script (type, write mapping, and per-partner data) - the `import-from-tft` mapping (also dropping the now-unused `partnerBudgetAverage` TFT source selection) `projectBudgetMin` is intentionally kept. ### Rework partner views - **Partners** (all-partners) view: replaced the **Deployment Expertise** column with **Categories** (the `partnerScope` field). - **Validated partners** view: added a **Languages Spoken** column. - Set view `position`s so the in-object view switcher orders **Validated → Applications → Partners**. ### Navigation order Reordered the "Partners" folder navigation items so the sidebar reads **Validated partners → Partner applications → Partners** (Partner content stays last). ### Also included The previously-pushed fix that excludes partners with an empty slug from the available-partners list. ## Notes - No deploy/sync performed. The view-column and navigation-ordering changes take effect once the app manifest is synced (`yarn twenty dev --once` locally). - The `deploymentExpertise` field itself is unchanged — only its column was removed from the all-partners view. |
||
|
|
ea84aabe4c |
chore(twenty-partners): refine design-doc skill doctrine (#21151)
## Summary Iterative refinements to the partner design-doc doctrine after running it on a second lead (TADA) and reviewing output side by side. Touches only the `twenty-partner-design-doc` skill files (doctrine + Claude Code wrapper); no runtime / app code. **What changed** - **Flag system:** emoji + short text label pairs only (`🔮 inf.`, **❓ open**, **⚠️ heavy**, **🛑 blocker**). Replaces the prior text-tag-only system; scannable, unambiguous. - **Section structure:** split into **Required** (always present) and **Conditional** (Views, Automations, Integrations, Reporting). Include conditional sections only when the client grounded them in the source. Number sequentially, no gaps. - **No filler placeholders:** banned `X was not named` / `left out on purpose` lists in body sections. Unknowns belong in Open questions, not as their own section or bullet. - **Functional cross-refs:** every `§N` reference is now a markdown anchor link `[§N](#n-section-slug)`, so a partner skimming the doc can navigate. Bare `§N` is banned. - **Bullets and tables over paragraphs**, with **Open questions** kept as a numbered list (so the partner can read items 1, 2, 3 with the client). - **Views & navigation** rendered as a tight `Surface | Shows | Audience` table. No view-type column — table / kanban / page layout is the partner's call, not a scoping decision. - **Data-model table** gains a `Source` column (`client` / `inf.`) for at-a-glance fact-vs-inference visibility. - **Business decisions over technical mechanics:** cut SDK / runtime internals that don't move the quote (Docker version, OAuth flavour, auto-system relations, env-var names, CI/CD workflow detail). - **Common-mistakes table** updated with rows for the new rules. - **SKILL.md self-check** expanded so the wrapper enforces all of the above before saving. ## Test plan - [ ] Re-read doctrine end-to-end for internal consistency - [ ] Verify the four canonical emoji + text pairs appear and no stray emoji flags remain - [ ] Confirm Required vs Conditional structure is internally consistent (no section listed in both) - [ ] Confirm functional-cross-ref rule appears in both Rules and Formatting and is reflected in the SKILL.md self-check - [ ] Confirm Views & navigation entry mandates the three-column table and bans a Type column - [ ] Confirm Common-mistakes table covers each new rule |
||
|
|
e721ebe300 |
chore(twenty-partners): bump app version to 0.3.3 (#21140)
Bumps the `twenty-partners` SDK app version 0.3.2 → 0.3.3 so `main` tracks what's deployed to prod. This is the deploy version for the partner-app changes that just landed: marketplace `partnerScope` exposure (#21126), the `submit-partner-application` endpoint + new Partner categories + migration (#21040), the marketplace card rebind (#21127), and the signup wizard (#21039). No code changes — version bump only. |
||
|
|
b2539f5b6a |
Prevent conditional availability variables from being used at runtime (#21110)
Fixes https://github.com/twentyhq/twenty/issues/21094 Conditional availability variables (`objectMetadataItem`, `numberOfSelectedRecords`, `objectPermissions`, operators like `everyEquals`/`none`, etc.) are compile-time-only constructs used in `conditionalAvailabilityExpression`. They were previously exported from `twenty-sdk/front-component`, which let developers mistakenly import them into runtime component code where they have no value. - Move conditional availability variables from `twenty-sdk/front-component` to `twenty-sdk/define`. - Add a build-time manifest validation (validate-conditional-availability-usage) that fails the build if these variables are imported/used outside of `conditionalAvailabilityExpression`. - Update the github-connector example app to register commands via dedicated *.command-menu-item.ts files instead of inline command config in front components. - Update docs (all locales) and test mocks to reflect the new import paths. |
||
|
|
4f47885054 |
feat(twenty-partners): submit-partner-application HTTP logic function (#21040)
## Summary Adds a public `POST /partner-applications` HTTP logic function on the twenty-partners SDK app that receives applications from the website wizard and idempotently upserts the Partner / Person / Company graph in the partners workspace. Also introduces the validated **Category** taxonomy on `partnerScope` (additive, prod-safe) plus the legacy→new migration tooling. Companion PR (website side): #21039 ### Logic function - `defineLogicFunction({ httpRouteTriggerSettings: { path: '/partner-applications', httpMethod: 'POST', isAuthRequired: false, forwardedRequestHeaders: ['x-application-secret'] } })`. - Authenticates via shared-secret header (`X-Application-Secret` ↔ `PARTNER_APPLICATION_SECRET` workspace variable). Twenty's `isAuthRequired: true` only accepts user-session JWTs, so the handler enforces auth itself. - Idempotent upsert keyed on `Person.emails.primaryEmail`: - missing email → create Company → Person → Partner - existing Person, no Partner → create Company + Partner, link - existing Person + Partner → update Partner fields; preserve staff-owned columns (`validationStage`, `reviewed`, `ranking`, `partnerTier`, `lastMatchAt`) by omitting them from the update - Create-time defaults preserved on resubmit: `slug = slugify(companyName)` ("YC Agency" → "yc-agency"), `reviewed = false`, `partnerTier = 'NEW'`. - Currency conversion to `{ amountMicros, currencyCode: 'USD' }` for `hourlyRate` + `projectBudgetMin`. ### Categories (`partnerScope`) — additive, prod-safe - Adds 5 validated category options — `ADVISORY`, `SOLUTIONING`, `DEVELOPMENT`, `HOSTING`, `SUPPORT` — to the `partnerScope` MULTI_SELECT **without removing** the legacy options (there is production data on them). Field relabeled **"Categories"**. The website form only emits the new values. - **Migration tooling** (run deliberately, *not* in CI): `scripts/migrate-partner-scope.ts` remaps existing records legacy→new — dry-run by default, `MIGRATE_APPLY=1` to write, two-pass (collect-then-apply, no mutate-while-paginating). `scripts/partner-scope-map.ts` is the single mapping source; `import-from-tft.ts` now routes imported scope through it so the TFT import never re-introduces retired values. Removing the legacy options is deferred until after the migration has run + been verified. ### applicationNotes - New `applicationNotes` TEXT field holds the wizard's single free-text "anything else" note (the handler passes it through directly). `deploymentExpertise` was dropped from the handler input/validation/builders (the column is retained for now, pending the same migration cleanup). ### Application variable - Declares `PARTNER_APPLICATION_SECRET` with `isSecret: true` so each workspace sets the value via Settings → Apps → Twenty Partners → Variables. Twenty encrypts at rest and merges the decrypted value into the handler's `process.env` at execution time (workspace value wins over container env). ### Code quality (from review) - One shared `slugify` (`scripts/slugify.ts`, the import's algorithm) used by both the handler and the import, so the `slug` identity key can't diverge across paths. - Unit-test tier: `vitest.unit.config.ts` (no `globalSetup`) + `yarn test:unit`, so the pure `mapLegacyScope` test runs without a live server (the integration suite stays server-backed). ## Demo 📹 _Screen recording of the wizard end-to-end (open → walk steps → submit → Partner record lands):_ https://github.com/user-attachments/assets/7458dd86-e3ff-47b5-9878-0eb134ff38e3 ### Tests - Integration tests against a local Twenty workspace: missing-/wrong-secret auth rejections, create flow (asserts slug + `reviewed: false` + `partnerTier: 'NEW'`), update-on-resubmit + staff-column preservation, new category values stored, `applicationNotes` stored, bad-input shape. - Pure `mapLegacyScope` unit test via `yarn test:unit` (no server). ## Test plan - [ ] Install / upgrade the app on the target workspace; set `PARTNER_APPLICATION_SECRET` in Settings → Apps → Twenty Partners → Variables - [ ] `curl -i -X POST <workspace-url>/s/partner-applications -H 'X-Application-Secret: <secret>' -H 'Content-Type: application/json' -d '{"firstName":"Test","lastName":"User","email":"test@example.com","companyName":"YC Agency","partnerScope":["ADVISORY"],"applicationNotes":"hi"}'` → `HTTP/1.1 201` + `{"ok":true,"created":true,"partnerId":"..."}` - [ ] Partner record shows `name: "YC Agency"`, `slug: "yc-agency"`, `validationStage: APPLICATION`, `reviewed: false`, `partnerTier: 'NEW'`, `partnerScope: ["ADVISORY"]`, `applicationNotes: "hi"` - [ ] Re-curl same email with `city: "Paris"` → `created: false`, `Partner.city` updated, staff-owned columns untouched - [ ] Wrong / missing secret → `200` + `{"ok":false,"reason":"unauthorized"}` - [ ] `yarn test:unit` green (no server); `yarn migrate:partner-scope` dry-run lists any legacy→new remaps without writing |
||
|
|
a3557373e6 |
feat(twenty-partners): expose partnerScope on list + by-slug endpoints (#21126)
## What Adds `partnerScope` (the partner **Categories** multi-select) to the output of the two public partner endpoints: - `list-available-partners` (`/s/partners`) - `get-partner-by-slug` (`/s/partner-by-slug`) Additive only — `deploymentExpertise` is kept, so existing consumers (the current live marketplace) are unaffected. ## Why Part of the partner marketplace rework. The website marketplace (companion branch `rk-rework-marketplace-cards`) consumes `partnerScope` to show/filter partner Categories. The new options + migration live in the signup app PR #21040. ## Merge order (we'll decide) Independent diff — can merge in any order. Couplings to keep in mind: - **Version line:** this branch and #21040 both bump the app `package.json` version; whoever merges second re-bumps. - **Deploy (not merge):** the partners app is deployed manually. Deploy the final combined app (this + #21040) and run `yarn migrate:partner-scope:prod` **before** the website is deployed. |
||
|
|
53392f9a16 |
feat(twenty-partners): partnerContent catalog + TFT import improvements (#20904)
## Summary Two related threads for the internal `twenty-partners` app: 1. **Redesign `partnerQuote` → `partnerContent`.** The object was mis-modeled as a sales/pre-invoice doc (`amount`, opportunity link). In TFT it's actually a marketing-content catalog — customer quotes, case studies, partner quotes, logos — moving through a production lifecycle. This renames it in place and reshapes it to mirror TFT's `CustomerContent`. 2. **Import tooling improvements** to the TFT importer + multi-env workflow. ## Changes **Schema (`partnerContent`)** - Rename `partnerQuote` → `partnerContent` (object, view, nav, relation fields, identifiers). - Add `contentType` MULTI_SELECT `[CUSTOMER_QUOTE, CASE_STUDY, PARTNER_QUOTE, LOGO]` and `interview` LINKS. - Add `customerCompany` / `customerPerson` relations; keep `partner`; drop the `opportunity` link (TFT has none). - Drop `amount`; rename the FILES field `quoteFile` → `documents` (`attachments` is a reserved morph-relation name). **Importer (`import-from-tft.ts`)** - Import the full content catalog (all types), not just `PARTNER_QUOTE`. - Map TFT `partnerTimezone` → `region`, default `languagesSpoken=[ENGLISH]`, and set `deploymentExpertise=[SELF_HOST]` when scope includes `HOSTING_ENVIRONMENT`. - Filter to partner-relevant records only: opportunities linked to a partner (20 of 164), content linked to a partner (10 of 22). Drops general sales-pipeline / customer-only noise. - Dedupe companies by **normalized domain** (Twenty's unique key), not just name — fixes duplicate-entry crashes when the same company arrives under different names. - Progress logging throughout. **Tooling** - `purge-soft-deleted` script (soft-deleted rows block re-imports via unique constraints). - Multi-env script variants (`*:prod`) selected via `ENV_FILE`. ## Testing Verified on a local Twenty instance and on `partner.twenty.com`: - 122 partners, 20 partner-linked opportunities, 10 partner-linked content (all types), 229 domain-deduped companies. - Schema confirmed via metadata introspection; `yarn twenty typecheck` clean. ## Notes - Renaming an installed object isn't a pure in-place migration on a server that already had `partnerQuote` — the working path is `uninstall → deploy → install` (safe here: prod had no data). 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
7ca9081efa |
Add application installation validation modale (#20907)
## After <img width="1512" height="829" alt="image" src="https://github.com/user-attachments/assets/231d4f0d-6052-4c4e-9a2a-0244d2b3832e" /> |
||
|
|
d602f35cbd |
feat(data-model): custom-indexes management UI and mutations (#20846)
## Summary
Brings indexes management into the per-object Settings tab as a section
under Search (no feature flag, advanced mode only). Admins can create /
delete non-unique indexes with the UI; apps can declare indexes in code
with `defineIndex`. Composite-typed fields are now indexable by picking
a specific sub-column (e.g. `Address > City`).
A few related polish items also land here (invite-user dropdown lands on
the Invite tab; standard warning callout above the new-index form).
## What ships
### UI — custom indexes on per-object Settings
- New section directly under Search, wrapped in
`AdvancedSettingsWrapper`.
- Filter dropdown on the search bar toggles system-index visibility
(shown by default since advanced mode).
- **+ Add Index** button (disabled with tooltip once the per-object cap
is reached) navigates to a dedicated `SettingsObjectNewIndex` page
(matches the field-creation pattern, not a modal):
- Field picker mirrors the webhook event-form layout (rows of dropdowns,
implicit trailing empty row).
- Composite fields surface their sub-properties (`Address > City`,
`Currency > Amount`, …).
- BTREE / GIN type selector.
- Standard warning Callout: "Use indexes sparingly — each one speeds
reads but slows writes."
- Trash icon on `isCustom: true` rows → confirmation modal →
`deleteOneIndex`.
### Server — `createOneIndex` / `deleteOneIndex` mutations
- Gated by `SettingsPermissionGuard(DATA_MODEL)`.
- `IndexMetadataService` wraps the existing migration runner via
`WorkspaceMigrationValidateBuildAndRunService` so the metadata row and
the SQL index land atomically.
- Validation: rejects empty fields, duplicate `(fieldMetadataId,
subFieldName)` pairs, fields not on the object, requires `subFieldName`
for composite parents, forbids `subFieldName` on scalar/relation,
enforces `MAX_CUSTOM_INDEXES_PER_OBJECT = 10`.
- Delete refuses on `isCustom: false` rows so system indexes can't be
removed via this API.
- Dedicated GraphQL exception handler maps each typed error to the right
transport error class.
### Composite sub-field indexing
- Adds `subFieldName: string | null` column to
`IndexFieldMetadataEntity` (fast instance command).
- The flat-entity flow (`UniversalFlatIndexFieldMetadata`,
`FlatIndexFieldMetadata`, `from-universal-flat-index-to-flat-index`,
runner column resolution) all carry `subFieldName` through.
- For composite parents, the runner uses
`computeCompositeColumnName({...}, property)` for the picked sub-column;
for non-composite parents, behavior is unchanged.
- The `'::'` separator encodes `(fieldMetadataId, subFieldName)` for
dedup on the wire; the frontend uses the same separator inside the
Select component's string value.
### Apps can declare indexes in code (`defineIndex`)
- New `IndexManifest` + `IndexFieldManifest` types in
`twenty-shared/application` wired into the `Manifest` type.
- `defineIndex` SDK helper + `IndexConfig`. CLI manifest builder +
extractor recognize `defineIndex` / `ManifestEntityKey.Indexes`.
- Server: `from-index-manifest-to-universal-flat-index` converter
resolves field IDs, validates composite/scalar `subFieldName` rules, and
delegates to `generateFlatIndexMetadataWithNameOrThrow` for the
deterministic name.
- Orchestrator wires the loop after the field-resolution pass;
per-object cap enforced inline against the manifest.
- Cascade on uninstall is automatic — when an app disappears its indexes
drop with it (universal-flat-entity diff handles it).
- Rich-app fixture ships a real `defineIndex` on `PostCard.status`,
exercising the full manifest → install path in CI.
### Closed for now (open later if needed)
- Apps cannot declare `isUnique` indexes — unique constraints stay with
the field-creation flow.
- Apps cannot use a partial-`indexWhereClause` — the UI surface keeps
the framework's hardcoded allowlist.
- UI cannot create unique or partial indexes either; same reasons.
### Cleanups along the way
- Reused the existing `getCompositeSubFieldLabel` +
`COMPOSITE_FIELD_SUB_FIELD_LABELS` (deleted the duplicates I'd created
early in the PR).
- Moved `MAX_CUSTOM_INDEXES_PER_OBJECT` to `twenty-shared/constants`
(single source for FE + BE).
- Replaced inline `isDefined(x) && x !== ''` with `isNonEmptyString`
(from `@sniptt/guards`).
- Hoisted the per-object fields Map + inlined the cap counter into the
indexes orchestrator loop (drops the install scan from O(indexes ×
totalFields) to O(totalFields + indexes)).
- Per design-feedback: page-based create flow (not a modal), filter
dropdown on the SearchInput (not a separate toggle), webhook-style
picker, field icons.
### Unrelated polish that lands here
- "Invite user" link in the multi-workspace dropdown now lands on the
Invite tab directly (`#invite`) instead of the first tab of the members
page.
## Test plan
- [ ] `npx nx typecheck twenty-server / twenty-front / twenty-sdk /
twenty-shared` — passes
- [ ] `npx nx lint:diff-with-main twenty-server / twenty-front` — clean
- [ ] `npx jest index-metadata.service.spec` — green
- [ ] `npx jest from-index-manifest-to-universal-flat-index` — green
(new converter spec, 8 cases)
- [ ] `npx vitest run
src/sdk/define/indexes/__tests__/define-index.spec.ts` (twenty-sdk) —
green (6 cases)
- [ ] `npx vitest run --config vitest.integration.config.ts -t
"rich-app"` — green (rich-app app-dev integration exercises the new
manifest path with the PostCard.status index)
- [ ] Advanced mode → Settings → any object → Settings tab → Indexes
section is visible under Search
- [ ] Create a single-field BTREE index, confirm SQL index exists
(verify via `pg_indexes`)
- [ ] Create a composite-field index (`Address > City`) and confirm the
column is `addressAddressCity`
- [ ] Create an index spanning two columns; column order matches the
picker order
- [ ] Attempt to create an 11th custom index → button is disabled with
tooltip
- [ ] Delete a custom index → confirmation modal → row disappears, PG
index dropped
- [ ] System indexes have no trash icon and are hidden by default
|
||
|
|
b8b115f4e3 |
FileStorageService Dedicated file and folder code flow + integrity check (#20831)
# Introduction
Next handling mimetype integrity check and checksum integrity check for
s3 storage type
Always expecting a trailing end slash when deleting a folder etc
## Application
Uninstalling an application now deletes all its related files
## File storage service
Making a distincton between folder path and file path
## Validation Pipeline
Every file operation in `FileStorageService.buildOnStoragePath` runs
through `validateResourcePath`, which chains three validators in order:
**1. `validateSafeRelativePath`** -- rejects path traversal attacks
| Input | Result | Error |
|---|---|---|
| `../../../etc/passwd` | Rejected | `Resource path must not contain
path traversal (..)` |
| `/etc/passwd` | Rejected | `Resource path must be relative, not
absolute` |
| `file\0.txt` | Rejected | `Resource path contains null bytes` |
| `..\\..\\etc\\passwd` | Rejected | `Resource path must not contain
backslashes` |
| _(empty)_ | Rejected | `Resource path must not be empty` |
**2. `validateFilenameIntegrity`** -- enforces safe characters, length
limits, extension required
| Input | Result | Error |
|---|---|---|
| `my folder/file.mjs` | Rejected | `A path segment contains invalid
characters...` |
| `Makefile` | Rejected | `Filename must have an extension` |
| `aaa...(256 chars).mjs` | Rejected | `A path segment exceeds the
maximum length of 255 characters` |
| `a/b/.../file.mjs` (1025+ chars) | Rejected | `Resource path exceeds
maximum length of 1024 characters` |
| `src/handlers/index.mjs` | Accepted | -- |
| `my-app/my_file.tsx` | Accepted | -- |
| `v1.0/module.config.mjs` | Accepted | -- |
Allowed characters per segment: `a-z`, `A-Z`, `0-9`, `.`, `-`, `_`
**3. `validateResourceExtension`** -- checks extension against the
`FileFolder` allowlist
| Input | FileFolder | Result | Error |
|---|---|---|---|
| `handler.js` | `BuiltLogicFunction` | Rejected | `Invalid file
extension. Allowed extensions: .mjs` |
| `card.tsx` | `BuiltFrontComponent` | Rejected | `Invalid file
extension. Allowed extensions: .mjs` |
| `script.js` | `PublicAsset` | Rejected | `Invalid file extension.
Allowed extensions: .png, .jpg, ...` |
| `index.mjs` | `BuiltLogicFunction` | Accepted | -- |
| `app.tsx` | `Source` | Accepted | -- |
| `photo.png` | `CorePicture` | Accepted | -- (unconfigured folder,
passes through) |
## Consumers
- **`FileStorageService`** -- calls `validateResourcePath`, throws
`FileStorageException` on failure (last-resort defense)
- **Resolver (`uploadApplicationFile`)** -- calls
`validateResourcePath`, throws `ApplicationException` on failure
(user-facing)
- **Flat validators** -- call `validateResourcePath`, push the error to
`validationResult.errors` (non-throwing, collects all errors)
All error messages are translated via Lingui `t` and returned in a
discriminated union `{ isValid: true } | { isValid: false, error: string
}`, letting each consumer decide how to handle failures.
|
||
|
|
3bda05ea57 |
[Breaking change] Prepare non-system permission flags (#20847)
# Summary Replaces the enum-keyed `permissionFlags: PermissionFlag[]` on roles with `permissionFlagUniversalIdentifiers: string[]` This unlocks mixing system flags (`SystemPermissionFlag.*`) with app-defined flags in a role config. This is a breaking change. Existing app source must switch to the new field. # Breaking changes - `RoleManifest.permissionFlags` removed. Use `RoleManifest.permissionFlagUniversalIdentifiers: string[]`. - `RoleConfig.permissionFlags` removed (was `PermissionFlagType[]`). Use `RoleConfig.permissionFlagUniversalIdentifiers: string[]`. - `PermissionFlagManifest` type removed from `twenty-shared/application`. - `PermissionFlag` re-export removed from `twenty-sdk/define`. `SystemPermissionFlag` is re-exported in its place. - Retargeting a permission flag between roles is now classified as delete + create instead of update ### Not in this PR - definePermissionFlag SDK function and top-level Manifest.permissionFlags catalog (apps defining their own custom flags). Until those land, permissionFlagUniversalIdentifiers only accepts SystemPermissionFlag.* UUIDs; arbitrary UUIDs fail validation. |
||
|
|
11b9f708d6 |
feat(twenty-partners): add partners app (#20792)
## Summary - Adds `twenty-partners`, a Twenty app that manages the partner matching pipeline: intake partner-eligible deals, assign vetted marketplace partners, and track the full funnel - Custom `Partner` object with availability, geo/language coverage, deployment expertise, and Calendly link - `matchStatus` SELECT field on Opportunity — 10 non-nullable states from `TO_BE_MATCHED` through `WON`/`LOST`, replacing a legacy boolean approach - Auto-match logic function: when `matchStatus` → `AUTO_MATCH`, assigns the longest-idle available partner and advances to `MATCHED`; falls back to `MANUAL_MATCH` with an audit note if no partner is free - Views: Waiting for match, Matches overview (Kanban by `matchStatus`), All matched deals, Partners, Opportunities - Roles: Partner Ops (internal, full CRUD) and Partner (external placeholder) - Idempotent seed scripts for demo partners and pipeline data ## Test plan - [ ] App installs cleanly on a fresh workspace (`yarn twenty dev`) - [ ] `matchStatus` Kanban grouping renders correctly in Matches overview - [ ] Waiting for match view filters to `TO_BE_MATCHED` and `MANUAL_MATCH` only - [ ] Auto-match logic assigns a partner and advances status - [ ] Seed scripts run without errors and are safe to re-run |
||
|
|
237a943947 |
Update twenty sdk commands (#20735)
Performs twenty-sdk cli command migration: Summary ``` ┌─────┬──────────────────────────┬────────────────────────────┬───────────────────────┐ │ # │ Old command │ New command │ Status │ ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤ │ 1 │ twenty dev [appPath] │ twenty dev [appPath] │ Unchanged (now also │ │ │ │ │ DEFAULT) │ ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤ │ 2 │ twenty dev --once │ twenty dev --once │ Unchanged │ │ │ [appPath] │ [appPath] │ │ ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤ │ 3 │ twenty dev --watch │ twenty dev [appPath] │ --watch flag removed │ │ │ [appPath] │ │ (was default) │ ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤ │ 4 │ twenty dev --verbose │ twenty dev --verbose │ Unchanged │ │ │ [appPath] │ [appPath] │ │ ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤ │ 5 │ twenty dev --debug │ twenty dev --debug │ Unchanged │ │ │ [appPath] │ [appPath] │ │ ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤ │ 6 │ twenty dev --debounceMs │ twenty dev --debounceMs │ Unchanged │ │ │ <ms> [appPath] │ <ms> [appPath] │ │ ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤ │ 7 │ twenty build [appPath] │ twenty dev:build [appPath] │ Deprecated → colon │ │ │ │ │ command │ ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤ │ 8 │ twenty build --tarball │ twenty dev:build --tarball │ Deprecated → colon │ │ │ [appPath] │ [appPath] │ command │ ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤ │ 9 │ twenty typecheck │ twenty dev:typecheck │ Deprecated → colon │ │ │ [appPath] │ [appPath] │ command │ ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤ │ 10 │ twenty logs [appPath] │ twenty dev:fn-logs │ Deprecated → colon │ │ │ │ [appPath] │ command │ ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤ │ 11 │ twenty logs -n <name> │ twenty dev:fn-logs -n │ Deprecated → colon │ │ │ [appPath] │ <name> [appPath] │ command │ ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤ │ 12 │ twenty logs -u <id> │ twenty dev:fn-logs -u <id> │ Deprecated → colon │ │ │ [appPath] │ [appPath] │ command │ ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤ │ 13 │ twenty exec [appPath] │ twenty dev:fn-exec │ Deprecated → colon │ │ │ │ [appPath] │ command │ ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤ │ 14 │ twenty exec -n <name> │ twenty dev:fn-exec -n │ Deprecated → colon │ │ │ [appPath] │ <name> [appPath] │ command │ ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤ │ 15 │ twenty exec -u <id> │ twenty dev:fn-exec -u <id> │ Deprecated → colon │ │ │ [appPath] │ [appPath] │ command │ ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤ │ 16 │ twenty exec -p <json> │ twenty dev:fn-exec -p │ Deprecated → colon │ │ │ [appPath] │ <json> [appPath] │ command │ ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤ │ 17 │ twenty exec │ twenty dev:fn-exec │ Deprecated → colon │ │ │ --postInstall [appPath] │ --postInstall [appPath] │ command │ ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤ │ 18 │ twenty exec --preInstall │ twenty dev:fn-exec │ Deprecated → colon │ │ │ [appPath] │ --preInstall [appPath] │ command │ ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤ │ 19 │ twenty add [entityType] │ twenty dev:add │ Deprecated → colon │ │ │ │ [entityType] │ command │ ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤ │ 20 │ twenty add --path <path> │ twenty dev:add --path │ Deprecated → colon │ │ │ [entityType] │ <path> [entityType] │ command │ └─────┴──────────────────────────┴────────────────────────────┴───────────────────────┘ App lifecycle commands ┌─────┬────────────────────────┬────────────────────────────┬─────────────────────────┐ │ # │ Old command │ New command │ Status │ ├─────┼────────────────────────┼────────────────────────────┼─────────────────────────┤ │ 21 │ twenty publish │ twenty app:publish │ Deprecated → colon │ │ │ [appPath] │ [appPath] │ command │ ├─────┼────────────────────────┼────────────────────────────┼─────────────────────────┤ │ 22 │ twenty publish --tag │ twenty app:publish --tag │ Deprecated → colon │ │ │ <tag> [appPath] │ <tag> [appPath] │ command │ ├─────┼────────────────────────┼────────────────────────────┼─────────────────────────┤ │ 23 │ twenty deploy │ twenty app:publish │ Deprecated → colon │ │ │ [appPath] │ --private [appPath] │ command + --private │ ├─────┼────────────────────────┼────────────────────────────┼─────────────────────────┤ │ 24 │ twenty install │ twenty app:install │ Deprecated → colon │ │ │ [appPath] │ [appPath] │ command │ ├─────┼────────────────────────┼────────────────────────────┼─────────────────────────┤ │ 25 │ twenty uninstall │ twenty app:uninstall │ Deprecated → colon │ │ │ [appPath] │ [appPath] │ command │ ├─────┼────────────────────────┼────────────────────────────┼─────────────────────────┤ │ 26 │ twenty uninstall -y │ twenty app:uninstall -y │ Deprecated → colon │ │ │ [appPath] │ [appPath] │ command │ └─────┴────────────────────────┴────────────────────────────┴─────────────────────────┘ Server commands ┌─────┬─────────────────────────┬─────────────────────────────┬──────────────────────┐ │ # │ Old command │ New command │ Status │ ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤ │ 27 │ twenty server start │ twenty docker:start │ Deprecated → colon │ │ │ │ │ syntax │ ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤ │ 28 │ twenty server start -p │ twenty docker:start -p │ Deprecated → colon │ │ │ <port> │ <port> │ syntax │ ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤ │ 29 │ twenty server start │ twenty docker:start --test │ Deprecated → colon │ │ │ --test │ │ syntax │ ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤ │ 30 │ twenty server stop │ twenty docker:stop │ Deprecated → colon │ │ │ │ │ syntax │ ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤ │ 31 │ twenty server stop │ twenty docker:stop --test │ Deprecated → colon │ │ │ --test │ │ syntax │ ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤ │ 32 │ twenty server status │ twenty docker:status │ Deprecated → colon │ │ │ │ │ syntax │ ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤ │ 33 │ twenty server status │ twenty docker:status --test │ Deprecated → colon │ │ │ --test │ │ syntax │ ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤ │ 34 │ twenty server logs │ twenty docker:logs │ Deprecated → colon │ │ │ │ │ syntax │ ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤ │ 35 │ twenty server logs -n │ twenty docker:logs -n │ Deprecated → colon │ │ │ <lines> │ <lines> │ syntax │ ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤ │ 36 │ twenty server logs │ twenty docker:logs --test │ Deprecated → colon │ │ │ --test │ │ syntax │ ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤ │ 37 │ twenty server reset │ twenty docker:reset │ Deprecated → colon │ │ │ │ │ syntax │ ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤ │ 38 │ twenty server reset │ twenty docker:reset --test │ Deprecated → colon │ │ │ --test │ │ syntax │ ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤ │ 39 │ twenty server upgrade │ twenty docker:upgrade │ Deprecated → colon │ │ │ [version] │ [version] │ syntax │ ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤ │ 40 │ twenty server upgrade │ twenty docker:upgrade │ Deprecated → colon │ │ │ --test [version] │ --test [version] │ syntax │ ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤ │ 41 │ twenty server │ twenty app:catalog-sync │ Deprecated → colon │ │ │ catalog-sync │ │ syntax │ ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤ │ 42 │ twenty server │ twenty app:catalog-sync │ Deprecated → colon │ │ │ catalog-sync -r <name> │ -r <name> │ syntax │ ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤ │ 43 │ twenty catalog-sync │ (removed) │ Removed (was already │ │ │ │ │ deprecated) │ └─────┴─────────────────────────┴─────────────────────────────┴──────────────────────┘ Remote commands ┌─────┬────────────────────────┬──────────────────────────┬──────────────────────────┐ │ # │ Old command │ New command │ Status │ ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤ │ 44 │ twenty remote add │ twenty remote:add │ Deprecated → colon │ │ │ │ │ syntax │ ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤ │ 45 │ twenty remote add --as │ twenty remote:add --as │ Deprecated → colon │ │ │ <name> │ <name> │ syntax │ ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤ │ 46 │ twenty remote add │ twenty remote:add │ Deprecated → colon │ │ │ --api-key <key> │ --api-key <key> │ syntax │ ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤ │ 47 │ twenty remote add │ twenty remote:add │ Deprecated → colon │ │ │ --api-url <url> │ --api-url <url> │ syntax │ ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤ │ 48 │ twenty remote add │ twenty remote:add │ Deprecated → colon │ │ │ --local │ --local │ syntax │ ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤ │ 49 │ twenty remote add │ twenty remote:add --test │ Deprecated → colon │ │ │ --test │ │ syntax │ ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤ │ 50 │ twenty remote list │ twenty remote:list │ Deprecated → colon │ │ │ │ │ syntax │ ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤ │ 51 │ twenty remote switch │ twenty remote:use [name] │ Deprecated → colon │ │ │ [name] │ │ syntax + renamed │ ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤ │ 52 │ twenty remote status │ twenty remote:status │ Deprecated → colon │ │ │ │ │ syntax │ ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤ │ 53 │ twenty remote remove │ twenty remote:remove │ Deprecated → colon │ │ │ <name> │ <name> │ syntax │ └─────┴────────────────────────┴──────────────────────────┴──────────────────────────┘ ``` --------- Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> |
||
|
|
c800eccc65 |
Slack workflow connector (#20427)
https://github.com/user-attachments/assets/5a746414-988b-473c-9401-b8863a3e1c15 https://github.com/user-attachments/assets/0cdebdb1-f7c8-43cb-beef-f279387b6ce9 https://github.com/user-attachments/assets/df31c631-0781-42d8-8e6e-e5a16573ee3b https://github.com/user-attachments/assets/6adaeae4-f3c9-4a5f-b0df-50c1f9a78428 --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: martmull <martmull@hotmail.fr> |
||
|
|
d345a6b2d6 |
Twenty fireflies integration (#20618)
Co-authored-by: martmull <martmull@hotmail.fr> |
||
|
|
d5ff9eb515 |
Create twenty app improvements (#20688)
create-twenty-app updates: - remove --example option - sync --once when scaffolding an applicaiton - rename --api-url option to --workspace-url - create a standalone page when scaffolding an app <img width="1494" height="765" alt="image" src="https://github.com/user-attachments/assets/0e35ed0c-b0aa-466c-9f56-7939294fd2cf" /> --------- Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> |
||
|
|
fc53f18a9f |
Twenty discord integration (#20530)
<img width="1290" height="777" alt="Screenshot 2026-05-14 at 8 55 02 AM" src="https://github.com/user-attachments/assets/37cde89b-b4c3-438a-8ccf-39621f9799b4" /> https://github.com/user-attachments/assets/cd8af6f9-f2e5-47cf-bf1f-57590bb358d1 https://github.com/user-attachments/assets/ff53c6b7-ee1a-42db-bbc5-d7df5b3fa6b0 https://github.com/user-attachments/assets/ddca2c16-774d-4672-aa61-05e878d8b2b7 |
||
|
|
c4e897a7b5 |
Improve linear app (#20453)
- Add front component form to create linear issue <img width="1512" height="831" alt="image" src="https://github.com/user-attachments/assets/ffbb223f-30a8-4c64-ac6d-002c29b604c1" /> <img width="1512" height="829" alt="image" src="https://github.com/user-attachments/assets/a5ed2464-35a9-4a60-804c-5f15eb0043b4" /> - improve marketplace Linear app page <img width="1302" height="834" alt="image" src="https://github.com/user-attachments/assets/cdec7ec2-953d-4a49-a797-5369834b03c1" /> - update admin settings to display non secret values <img width="861" height="473" alt="image" src="https://github.com/user-attachments/assets/41dadf02-aa5d-4eb6-befe-0ad8ad4049b2" /> |
||
|
|
773245fa65 |
Isolate twenty apps from nx project (#20406)
- avoids importing twenty-shared or else in twenty-apps applications - update and add workflow action in twenty linear app |
||
|
|
617f571400 |
20215 convert application variable to a syncable entity (#20269)
## Summary - Converts applicationVariable from a bespoke sync path to a proper SyncableEntity, unifying it with the workspace migration pipeline used by all other manifest-managed entities (agent, skill, frontComponent, webhook, etc.) - Removes the upsertManyApplicationVariableEntities method and its direct-DB-mutation approach in favor of the standard validate → build → run action handler pipeline - Adds universalIdentifier, deletedAt columns and makes applicationId NOT NULL via an instance command migration ## Motivation Before this change, applicationVariable was the only manifest-managed entity that bypassed ApplicationManifestMigrationService.syncMetadataFromManifest(). It used a bespoke service method called directly from syncApplication(), creating two mental models, two validation styles, and two cache invalidation patterns. Now there's one unified pipeline for all manifest entities. ## What changed ### Entity refactor: - ApplicationVariableEntity now extends SyncableEntity (gains universalIdentifier, non-nullable applicationId with CASCADE, soft-delete via deletedAt) ### New flat entity layer (flat-application-variable/): - Type, maps type, editable properties constant, entity-to-flat converter, cache service, module ### New migration pipeline wiring: - Manifest converter (fromApplicationVariableManifestToUniversalFlatApplicationVariable) - Validator service (FlatApplicationVariableValidatorService) - Builder service (WorkspaceMigrationApplicationVariableActionsBuilderService) - Create/Update/Delete action handlers with secret encryption hooks - Registered in orchestrator, builder module, runner module, and all type registries ### Removed bespoke path: - Deleted upsertManyApplicationVariableEntities from ApplicationVariableEntityService - Removed its call from ApplicationSyncService.syncApplication() - Kept update() (operator-set value at runtime) and getDisplayValue() (runtime display) ### Database migration: - Instance command to add columns, backfill universalIdentifier, enforce NOT NULL constraints, and update indexes ## Test plan - npx nx typecheck twenty-server passes (0 errors) - Unit tests pass (application-variable.service.spec.ts, build-env-var.spec.ts) - Install an app with applicationVariables in its manifest → variables appear with correct universalIdentifier - Update app manifest (add/remove/modify a variable) → migration pipeline handles diff correctly - Operator-set value via update endpoint persists correctly with encryption - Uninstall app → variables cascade-deleted - app dev --once on example app syncs without errors |
||
|
|
53fdac1417 |
feat(apps): split AI tool and workflow action triggers in LogicFunction manifest (#20208)
## Summary Replaces the bolted-on `isTool` + `toolInputSchema` fields on `LogicFunctionManifest` with two distinct, opt-in triggers that align with the existing `cron` / `databaseEvent` / `httpRoute` trigger pattern: - **`toolTriggerSettings`** — exposes the function as an AI tool (chat / MCP / function calling). Uses standard JSON Schema (the format LLMs natively understand). - **`workflowActionTriggerSettings`** — exposes the function as a step in the visual workflow builder. Uses Twenty's rich `InputSchema` so the builder can render proper `FieldMetadataType`-aware editors, variable pickers, labels, and an optional `outputSchema`. A function can opt into none, one, or both. Each surface gets the schema format appropriate for it. ### Why `isTool: true` previously exposed the function as both an AI tool AND a workflow node, with the same JSON Schema feeding both — but the workflow builder really wants Twenty's `InputSchema` (with `CURRENCY`, `RELATION`, `EMAILS`, etc.) and the AI surface really wants standard JSON Schema. Today the workflow builder hacks around this by treating JSON Schema as `InputSchema`, which silently breaks for any non-primitive field type. Splitting the triggers fixes that and lets each surface evolve independently. ### Migration - **Fast** instance command adds the two new nullable columns. - **Slow** instance command backfills `toolTriggerSettings` + `workflowActionTriggerSettings` from `isTool=true` rows (preserving today's both-surfaces behaviour) then drops the legacy columns. ### Stacked Stacked on top of #20181. Merge that first, then this. ## Test plan - [ ] CI green (oxlint, typecheck, jest, vitest) - [ ] Run `--include-slow` upgrade against a workspace with existing `isTool=true` logic functions; verify both new columns populated and old columns dropped - [ ] Verify AI chat sees migrated tool functions (Linear create-issue, Exa search) and can call them with the JSON Schema - [ ] Add an AI-tool function from the Settings UI (toggles `toolTriggerSettings`) and verify it shows up in chat - [ ] Add a workflow-action function from the Settings UI (toggles `workflowActionTriggerSettings`) and verify it appears in the workflow node picker - [ ] In the workflow builder, edit a `LOGIC_FUNCTION` step and verify input fields render (no more JSON-Schema-as-InputSchema hack) - [ ] Try defining a function with no triggers in the SDK and verify `defineLogicFunction` rejects it 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: martmull <martmull@hotmail.fr> |
||
|
|
820f97f53d |
[Headless Front component] Support multiple selected record (#20268)
# Introduction Support multiple selected record ids for headless front components ### Changes **Added:** - `recordIds: string[]` field to `FrontComponentExecutionContext` - `useRecordIds()` hook to get all selected record IDs **Deprecated:** - `recordId` field - use `recordIds` instead - `useRecordId()` hook - use `useRecordIds()` instead Backward compatibility is preserved |
||
|
|
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" /> |
||
|
|
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" /> |
||
|
|
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>
|
||
|
|
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" /> |
||
|
|
3db1af9a17 |
fix(logic-function): forward raw request body for HMAC signature verification (#20061)
## Summary - Add optional `rawBody?: string` to `LogicFunctionEvent` and forward it from the route trigger so HMAC-based webhook signatures (GitHub's `X-Hub-Signature-256`, Stripe, …) can be verified by user logic functions. - Update `github-connector`'s `getRawBodyForSignature` to prefer `event.rawBody` (with the existing string/base64/null fallbacks kept for older runtimes). ## Why GitHub computes `X-Hub-Signature-256` over the **raw bytes** of the request body. The receiver must verify against those exact bytes — key order, whitespace and unicode escaping all matter, so the parsed JSON body cannot be re-serialized to them. Today the route trigger calls `extractBody(request)` which returns the parsed object only. NestJS already preserves the raw body on `request.rawBody` (the app is bootstrapped with `rawBody: true` in `main.ts`), but it was never propagated into `LogicFunctionEvent`. As a result the github-connector's webhook handler always took the "raw body unavailable" branch and rejected every delivery (after #19961 / 962c2b3c14). With this change, signature verification can succeed end-to-end. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
0c5c9c844e |
feat(github-connector): exclude self-reviews from review counts (#20050)
## Summary - Adds an `isSelfReview` boolean to the consolidated `PullRequestReview` record (`true` when the reviewer is the same contributor as the PR author). - Filters `isSelfReview === true` rows out of the top-reviewers leaderboard (`top-contributors`) and per-contributor review stats (`contributor-stats`) so contributors are credited only for reviews on **other people's** PRs. - Both the live ingestion path (`fetch-prs`) and the recompute job (`recompute-pull-request-reviews`) now thread `prAuthorId` to `buildConsolidatedRow`, so re-running either is sufficient to backfill existing rows — no extra migration needed. ## Test plan - [x] `yarn test src/modules/github/pull-request-review/utils/consolidate-reviews.integration-test.ts` — 20 tests passing, including new coverage for the `isSelfReview` helper and `buildConsolidatedRow` payload. - [ ] After merge: re-run `fetch-prs` (or `recompute-pull-request-reviews`) once on a target workspace to backfill `isSelfReview` on existing `PullRequestReview` rows, then verify the top-reviewers leaderboard no longer credits self-reviews. Made with [Cursor](https://cursor.com) |
||
|
|
0bb3660844 |
chore(twenty-sdk): shrink logic-function bundles via stubbing (#20033)
## Summary
Logic-function bundles produced by the SDK CLI were ~1.2 MB each (source
maps ~3.1 MB) because esbuild was inlining `twenty-sdk/define` and its
transitive dependencies (zod + locales, twenty-shared, etc.). Those
`define*` factories are pure build-time metadata used only by the
manifest extractor — the Lambda runtime only ever invokes
`default.config.handler`, so the factories are dead weight at runtime.
This PR shrinks the bundles to ~9.5 KB each (~99% reduction) without
changing runtime behaviour.
## What changes
- **Stub `twenty-sdk/define` at user-app build time.** New esbuild
plugin
(`packages/twenty-sdk/src/cli/utilities/build/common/plugins/stub-twenty-sdk-define.plugin.ts`)
intercepts every import of `twenty-sdk/define` during user-app builds
and replaces it with a tiny virtual module:
- Factory functions (`defineLogicFunction`,
`definePostInstallLogicFunction`, …) become `(config) => ({ success:
true, config, errors: [] })`.
- Enums and helpers become `Proxy`-based no-ops.
- Wired into both the one-shot build (`build-application.ts`) and the
watcher (`esbuild-watcher.ts`), for logic functions and front
components.
- **New runtime barrel `twenty-sdk/logic-function`.** Re-exports only
the types logic-function authors need (`InstallPayload`, `RoutePayload`,
`CronPayload`, `DatabaseEventPayload`, `LogicFunctionConfig`,
`InputJsonSchema`, …). Compiled `.mjs` is 36 bytes. Wired into Vite,
Rollup `.d.ts` bundling, `package.json#exports`, and `typesVersions`.
- **Lint enforcement.** Added an oxlint `no-restricted-imports` rule
that forbids `twenty-shared` / `twenty-shared/*` imports from
`**/*.logic-function.ts` and `**/logic-functions/**/*.ts`, with a help
message pointing at the new barrel. Applied to the `create-twenty-app`
template and to `github-connector`, `hello-world`, `postcard`.
- **Migrated existing sources.** All logic-function files across
`community/{github-connector, apollo-enrich}`, `examples/{hello-world,
postcard}`, and `internal/{twenty-for-twenty, self-hosting, exa}` now
import types from `twenty-sdk/logic-function` instead of
`twenty-sdk/define` or `twenty-shared/*`. Renamed leftover
`InstallLogicFunctionPayload` references to `InstallPayload`.
## Why this is safe
- `define*` exports from `twenty-sdk/define` are metadata factories
whose call expressions are statically inspected by the manifest
extractor (`manifest-extract-config.ts`). They're never evaluated at
runtime — the Lambda executor only walks `default.config.handler`
(`logic-function-drivers/constants/executor/index.mjs`).
- The stub keeps the same call shape (`{ success, config, errors }`), so
any logic-function module that re-exports
`defineX(config).config.handler` still resolves to the user's handler at
runtime.
- Front-component bundles are unaffected by the stub because the
pre-existing JSX transform plugin
(`jsx-transform-to-remote-dom-worker-format-plugin.ts`) unwraps
`defineFrontComponent(...)` earlier in the pipeline. That's intentional
— front-component bloat is React/Preact, not in scope here.
## Measurements (github-connector)
| Asset | Before | After |
|---|---|---|
| `*.logic-function.mjs` | ~1.2 MB | ~9.5 KB |
| `*.logic-function.mjs.map` | ~3.1 MB | ~22 KB |
|
||
|
|
2a3c759928 |
Removing community apps in favor of npm-distributed apps (#20029)
## Context With the registry pattern around `Twenty Applications` and the upcoming marketplace, app distribution can be decoupled from the core repo. Pulling apps from npm instead of shipping them in-tree means: - Contributors don't clone / index / build code for apps they'll never touch - Each app can version, release, and iterate independently of the core cadence - We stop carrying apps that are effectively unmaintained in the monorepo ## How This PR removes the 11 community apps from `packages/twenty-apps/community/` ## Note - **Marketplace apps** must be published on npm and pulled from there — no app code living in this repo just to be distributed - **Showcase / example apps** stay as living examples of what the SDK can do under `example/` - **Officially-maintained marketplace apps** (like the new `github-connector`?) stay in the repo under `internal/`. Some may be pre-installed on new workspaces alongside the Standard app |
||
|
|
4f938aa097 |
feat(app): infrastructure for pre-installed apps (#19973)
**PR 1 of 2.** Follow-up PR ships the Exa app, sets it as a default
pre-installed app, and removes the current `WebSearchTool` /
`WebSearchService` / `ExaDriver`. This PR adds the plumbing; no
user-visible change yet.
## Summary
- Server admins can declare a list of npm app packages to auto-install
on every new workspace and backfill onto existing workspaces via CLI.
- Server-level secrets (like Exa's API key) live on the
`ApplicationRegistration` (one row per server, encrypted) and are
injected into logic function execution env at runtime. No more
per-workspace storage of global secrets.
- A generic `POST /app/billing/charge` endpoint lets app logic functions
emit workspace usage events for metered features. Exa uses it in PR 2;
future apps (call recorder, etc.) reuse it.
- `LogicFunctionToolProvider` tool name prefix changes `logic_function_`
→ `app_`. Shorter, accurate (they come from installed apps).
## What's in this PR
**Logic function executor — server-level variables**
- `LogicFunctionExecutorService.getExecutionEnvVariables` now resolves
env vars in the order: hardcoded defaults →
`ApplicationRegistrationVariable[]` (server-level) →
`ApplicationVariable[]` (workspace-level override). The manifest
`serverVariables` schema has existed; this closes the loop.
**Config**
- `PRE_INSTALLED_APPS` — comma-separated list of npm packages. Default:
empty.
**\`PreInstalledAppsService\`** (new module)
- \`onApplicationBootstrap()\` — fetches each package's manifest from
the app registry CDN, upserts an \`ApplicationRegistration\`, and seeds
declared \`serverVariables\` from matching env vars (e.g.
\`EXA_API_KEY\` env → encrypted registration variable).
- \`installOnWorkspace(workspaceId)\` — installs all pre-installed apps
on a single workspace. Tolerates per-app failures.
**Auto-install on new workspace activation**
- \`WorkspaceService.prefillCreatedWorkspaceRecords\` invokes
\`installOnWorkspace\` after prefilling standard records. Non-blocking
on failure.
**Backfill CLI command**
- \`install-pre-installed-apps\` — iterates active and suspended
workspaces, installs pre-installed apps that aren't yet installed.
Idempotent. Run after changing \`PRE_INSTALLED_APPS\`.
**App billing endpoint**
- \`POST /app/billing/charge\`. Authenticated via \`APPLICATION_ACCESS\`
token (already injected into logic function execution env as
\`DEFAULT_APP_ACCESS_TOKEN\`). Body: \`{ creditsUsedMicro, quantity,
unit, operationType, resourceContext? }\`. Emits \`USAGE_RECORDED\` with
\`applicationId\` as \`resourceId\`. Generic — reusable by any app.
**Tool name prefix**
- \`LogicFunctionToolProvider.buildLogicFunctionToolName\` now produces
\`app_<name>\` instead of \`logic_function_<name>\`. Only affects tools
sourced from logic functions; other tool providers unchanged.
## Stats
- 16 files, +501 / −2
- 7 new files (1 command, 1 service × 2, 1 controller, 1 DTO, 2 modules)
- Typecheck: 7 pre-existing errors, zero new
- Prettier clean
## Behavior deltas
- **\`PRE_INSTALLED_APPS\` default = empty**: existing servers see no
change on merge.
- **\`ApplicationRegistrationVariable\` is now read by the executor**:
apps that were using manifest \`serverVariables\` but expecting them to
be ignored by the executor will now see them injected. No apps ship with
\`isTool: true\` logic functions today, so this is latent — first
consumer is Exa in PR 2.
- **Tool prefix**: currently no logic-function tools are named
\`logic_function_*\` in any production flow. The prefix change affects
only future tools emitted by \`LogicFunctionToolProvider\`.
## Risks
- **CDN unavailability at startup**: if the app registry CDN is down,
\`ensureRegistrationsExist\` logs warnings but doesn't block server
start. Installation on new workspaces during this window will find no
registrations and log a non-blocking error. Backfill command can retry
after CDN recovers.
- **Cold-start overhead**: \`ensureRegistrationsExist\` is called once
per process on bootstrap. Current configurable default is empty, so zero
overhead. When an admin sets \`PRE_INSTALLED_APPS\`, they accept one
HTTP call per package at boot.
- **Server-level variables flow**:
\`ApplicationRegistrationVariable.encryptedValue\` is shared by all
workspaces of a server. Appropriate for a single-tenant Exa key. Not
appropriate for per-tenant keys — those go in workspace-level
\`ApplicationVariable\` and override.
## Test plan
- [ ] \`npx nx typecheck twenty-server\` passes (verified: 7
pre-existing unrelated errors, zero new)
- [ ] Set \`PRE_INSTALLED_APPS=@twenty-apps/hello-world\` (or any real
npm-published app), \`HELLO_WORLD_API_KEY=xxx\`, restart server:
\`ApplicationRegistration\` row is upserted,
\`ApplicationRegistrationVariable\` for HELLO_WORLD_API_KEY is populated
(encrypted).
- [ ] Create a new workspace: the app is auto-installed,
\`ApplicationEntity\` row created, \`LogicFunctionEntity\` rows created.
- [ ] Existing workspace: run \`yarn nx run twenty-server:command
install-pre-installed-apps\`: apps install across all workspaces,
idempotent on re-run.
- [ ] Trigger a logic function that reads
\`process.env.HELLO_WORLD_API_KEY\`: value resolves from the
server-level \`ApplicationRegistrationVariable\`.
- [ ] Log a charge from the handler: \`POST /app/billing/charge\` with
\`Authorization: Bearer \$DEFAULT_APP_ACCESS_TOKEN\` body
\`{creditsUsedMicro: 1000, quantity: 1, unit: "INVOCATION",
operationType: "WEB_SEARCH"}\` → returns \`{success: true}\`,
\`USAGE_RECORDED\` event emitted with correct
\`resourceId=applicationId\`.
- [ ] Tool name generated by \`LogicFunctionToolProvider\` starts with
\`app_\`.
## What's NOT in this PR (PR 2 scope)
- The Exa app itself (\`packages/twenty-apps/...\` directory)
- Removing \`WebSearchTool\`, \`WebSearchService\`, \`ExaDriver\`,
\`web-search\` module
- Removing \`WEB_SEARCH_DRIVER\` config var
- Removing the current \`exa_web_search\` entry in
\`ActionToolProvider\`
- Chat preload list updated to \`app_exa_web_search\`
- Frontend \`getToolDisplayMessage\` branch for \`app_exa_web_search\`
- Setting \`PRE_INSTALLED_APPS\` default to include \`@twenty-apps/exa\`
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
a445f4a6fa |
feat(sdk): add definePageLayoutTab for extending existing page layouts (#20004)
## Summary
Introduces `definePageLayoutTab` so apps can attach a single tab (with
optional widgets) to an **existing** `pageLayout` referenced by
`pageLayoutUniversalIdentifier`. The parent layout can be standard, from
the same app, or from another app — mirroring how `defineField`
references an object via `objectUniversalIdentifier`.
This complements `definePageLayout`: use `definePageLayout` when you own
the entire layout, use `definePageLayoutTab` when you only want to add
to one.
```ts
import { definePageLayoutTab, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
export default definePageLayoutTab({
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001',
pageLayoutUniversalIdentifier: 'STANDARD-OR-OTHER-APP-PAGE-LAYOUT-UUID',
title: 'Hello World',
position: 1000,
icon: 'IconWorld',
layoutMode: PageLayoutTabLayoutMode.CANVAS,
widgets: [/* ... */],
});
```
## Changes
- **twenty-shared**: new top-level `pageLayoutTabs:
PageLayoutTabManifest[]` on `Manifest`, optional
`pageLayoutUniversalIdentifier` on `PageLayoutTabManifest`, new
`SyncableEntity.PageLayoutTab`.
- **twenty-sdk**:
- new `definePageLayoutTab` + `PageLayoutTabConfig` exports;
- manifest extraction wiring (`TargetFunction.DefinePageLayoutTab`,
`ManifestEntityKey.PageLayoutTabs`);
- dev-mode label/state for the new entity;
- CLI scaffold (`getPageLayoutTabBaseFile`) + unit tests for `npx
twenty-cli add`.
- **twenty-server**: convert top-level `pageLayoutTabs` (and their
widgets) into universal flat entities in
`computeApplicationManifestAllUniversalFlatEntityMaps`. Cross-app FK
validation on `pageLayoutUniversalIdentifier` is already handled by the
existing `FlatPageLayoutTab` validator.
- **docs**: new `definePageLayoutTab` accordion in `apps/layout.mdx`
with usage example and guidance vs `definePageLayout`.
- **CI / rich-app fixture**: `extra-tab.page-layout-tab.ts` exercises
the new flow with a front-component widget; `expected-manifest.ts` and
`manifest.tests.ts` updated.
|
||
|
|
0d996a5629 |
Resend app improvements (#19986)
## Summary Major overhaul of the `twenty-for-twenty` Resend app to make sync more reliable, observable, and feature-complete. ### SDK upgrade - Bumps `twenty-sdk` to `2.0.0` and `twenty-client-sdk` to `1.23.0-canary.1` - Pins React back to `^18.2.0` to match the SDK ### Sync engine rewrite - Splits the single `sync-resend-data` job into 4 staggered cron-driven logic functions: **Emails**, **Contacts**, **Broadcasts (+ segments + dependencies)**, **Templates** — each running every 5 minutes on a different minute offset with per-slot timeouts - Adds a new `ResendSyncCursor` object + `with-sync-cursor` orchestration so each step persists its progress, last run timestamp, and last run status - Introduces an `INITIAL_SYNC_MODE` app variable + `resend-initial-sync-mode-monitor` that flips to intermediate sync once every cursor is empty (intermediate sync only refetches the last 7 days of emails) - Stops auto-creating People from Resend contacts; instead backfills `personId` on Resend contacts/emails by matching existing People by email - Renames `on-*-deleted` handlers to `on-*-destroyed` and removes from Resend on destroy (not soft delete) - Adds rate-limit retry, paginated `for-each-page`, typed-client, and existing-IDs lookup helpers ### New objects & fields - New `ResendTopic` object with relation to `ResendBroadcast` (+ navigation menu item, view, page layout) - New `ResendSyncCursor` object (step / cursor / last run at / last run status) - Adds `html` and `text` fields on `ResendBroadcast`; removes raw `htmlBody`/`textBody`/`tags` from `ResendEmail` ### New UI - **Sync Status standalone page** (`ResendSyncStatus` front component + nav item) showing live cursor / last run state per step - **Person Resend Email Stats** front component: deliverability rate + per-status breakdown with progress bars - **Email Broadcast HTML viewer** front component renders an individual email against its parent broadcast's HTML; new dedicated **Broadcast HTML viewer** - Adds Resend Broadcast record page layout (Home / Preview / Timeline / Tasks / Notes / Files tabs) ### Tests - ~25 new unit / integration test files covering sync utilities, cursor lifecycle, webhook handler, email-stats computation, sync-status page resolution, and rate-limit retry - Replaces legacy `fetch-all-paginated` tests with `for-each-page` tests |
||
|
|
3ebeb3a3e8 |
feat(community): add github-connector example app (#19961)
## Summary Adds a new community app at `packages/twenty-apps/community/github-connector` that demonstrates a complete, production-style GitHub integration built on the Twenty SDK. It is extracted (and decoupled) from the internal `twenty-eng` workspace so external developers can use it as a reference for their own connectors. What it ships: - **Six synced objects**: `pullRequest`, `pullRequestReview`, `pullRequestReviewEvent`, `issue`, `projectItem`, `engineer` - **Logic functions** for periodic backfills (PRs, reviews, issues, project items, contributors) and a single signed-webhook route trigger (`POST /github/webhook`) that performs idempotent upserts for `pull_request`, `pull_request_review`, `issues`, and `projects_v2_item` events - **Views, navigation menu items and a GitHub folder** so the data is discoverable in the UI out of the box - **Configurable repos / project numbers** via `GITHUB_REPOS` and `GITHUB_PROJECT_NUMBERS` application variables — no hardcoded org ## Authentication Two interchangeable modes (PAT preferred for quick setup, GitHub App recommended for production): 1. **Personal Access Token** — set `GITHUB_TOKEN`. Used as-is for both REST and GraphQL. 2. **GitHub App** — set `GITHUB_APP_ID`, `GITHUB_APP_PRIVATE_KEY`, `GITHUB_APP_INSTALLATION_ID`. Issues a signed JWT, exchanges it for a short-lived installation token, and caches the token until expiry. Webhook signature verification (`X-Hub-Signature-256`) is enforced when `GITHUB_WEBHOOK_SECRET` is set. ## Notes - Built on `twenty-sdk@2.0.0` / `twenty-client-sdk@2.0.0` - Decoupled from internal modules (`quality/bug`, `discord`, `release`, `code-build`, `project-management`) — `mustBeQa` is inlined and a local `github` nav folder replaces shared ones - `npx twenty typecheck`, `yarn lint`, and `npx twenty build` all run cleanly - Includes a comprehensive README with setup, env vars, webhook configuration, and the auth resolution flow |
||
|
|
b010599000 |
fix(server): preserve kanban/calendar fields in view manifest sync (#19946)
## Summary The `fromViewManifestToUniversalFlatView` converter hardcoded five view fields to `null` instead of reading them from the manifest: - `mainGroupByFieldMetadataUniversalIdentifier` - `kanbanAggregateOperation` - `kanbanAggregateOperationFieldMetadataUniversalIdentifier` - `calendarLayout` - `calendarFieldMetadataUniversalIdentifier` As a result **any** Kanban view in an app manifest is rejected by `validateFlatViewCreation` with `"Kanban view must have a main group by field"`, and any Calendar view would trip the `view.entity.ts` check constraint requiring `calendarLayout` + `calendarFieldMetadataId` to be non-null. Discovered while trying to install [`twenty-crm-meeting-baas`](https://github.com/Meeting-BaaS/twenty-crm-meeting-baas) which ships a Kanban view. ## Changes - **Server converter**: read all five fields from the manifest (with `?? null` fallback). - **`ViewManifest` type** (`twenty-shared`): add the five fields so SDK users can set them type-safely. - **Move `ViewCalendarLayout`** from `twenty-server` to `twenty-shared` so the manifest type can reference it. Seven import sites updated; the front-end imports via generated GraphQL types and is unaffected. - **Unit tests**: extend `from-view-manifest-to-universal-flat-view.util.spec.ts` with preservation + null-default cases for both Kanban and Calendar (5 tests total). - **Regression coverage**: add a Kanban view (`post-cards-by-status.view.ts`) to the `rich-app` fixture grouped by the existing `status` SELECT field. The existing `applications-install-delete-reinstall` e2e test now exercises the Kanban path end-to-end — a future regression here would fail CI. Note: `expected-manifest.ts` and the `views.length` assertion in `manifest.tests.ts` were updated to reflect the new fixture view. ## Test plan - [x] `nx test twenty-server -- from-view-manifest-to-universal-flat-view` → 5/5 pass - [x] `nx typecheck twenty-shared` / `twenty-sdk` / `twenty-server` → no new errors (one pre-existing unrelated error in `admin-panel.module-factory.ts`) - [x] `nx lint twenty-shared` / `twenty-sdk` → clean - [x] Manual install of the Meeting BaaS app on a dev workspace succeeds with the Kanban view after this fix - [ ] CI: SDK e2e `applications-install-delete-reinstall` passes against the new fixture view - [ ] CI: integration test `calendar-field-deactivation-deletes-views` still passes after the enum move 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> |
||
|
|
eb1ca1b9ec |
perf(sdk): split twenty-sdk barrel into per-purpose subpaths to cut logic-function bundle ~700x (#19834)
## Summary
Logic-function bundles produced by the twenty-sdk CLI were ~1.18 MB even
for a one-line handler. Root cause: the SDK shipped as a single bundled
barrel (`twenty-sdk` → `dist/index.mjs`) that co-mingled server-side
definition factories with the front-component runtime, validation (zod),
and React. With no `\"sideEffects\"` declaration on the SDK package,
esbuild had to assume every module-level statement could have side
effects and refused to drop unused code.
This PR restructures the SDK so consumers' bundlers can tree-shake at
the leaf level:
- **Reorganized SDK source.** All server-side definition factories now
live under `src/sdk/define/` (agents, application, fields,
logic-functions, objects, page-layouts, roles, skills, views,
navigation-menu-items, etc.). All front-component runtime
(components, hooks, host APIs, command primitives) lives under
`src/sdk/front-component/`. The legacy bare `src/sdk/index.ts` is
removed; the bare `twenty-sdk` entry no longer exists.
- **Split the build configs by purpose / runtime env.** Replaced
`vite.config.sdk.ts` with two purpose-specific configs:
- `vite.config.define.ts` — node target, externals from package
`dependencies`, emits to `dist/define/**`
- `vite.config.front-component.ts` — browser/React target, emits to
`dist/front-component/**`
Both use `preserveModules: true` so each leaf ships as its own `.mjs`.
- **\`\"sideEffects\": false\`** on `twenty-sdk` so esbuild can drop
unreferenced re-exports.
- **\`package.json\` exports + \`typesVersions\`** updated: dropped the
bare \`.\` entry, added \`./front-component\`, and pointed \`./define\`
at the new per-module dist layout.
- **Migrated every internal/example/community app** to the new subpath
imports (`twenty-sdk/define`, `twenty-sdk/front-component`,
`twenty-sdk/ui`).
- **Added \`bundle-investigation\` internal app** that reproduces the
bundle bloat and demonstrates the fix.
- Cleaned up dead \`twenty-sdk/dist/sdk/...\` references in the
front-component story builder, the call-recording app, and the SDK
tsconfig.
## Bundle size impact
Measured with esbuild using the same options as the SDK CLI
(\`packages/twenty-apps/internal/bundle-investigation\`):
| Variant | Imports | Before | After |
| ----------------------- |
------------------------------------------------------- | ---------- |
--------- |
| \`01-bare\` | \`defineLogicFunction\` from \`twenty-sdk/define\` |
1177 KB | **1.6 KB** |
| \`02-with-sdk-client\` | + \`CoreApiClient\` from
\`twenty-client-sdk/core\` | 1177 KB | **1.9 KB** |
| \`03-fetch-issues\` | + GitHub GraphQL fetch + JWT signing + 2
mutations | 1181 KB | **5.8 KB** |
| \`05-via-define-subpath\` | same as \`01\`, via the public subpath |
1177 KB | **1.7 KB** |
That's a ~735× reduction on the bare baseline. Knock-on benefits for
Lambda warm + cold starts, S3 upload size, and \`/tmp\` disk usage in
warm containers.
## Test plan
- [x] \`npx nx run twenty-sdk:build\` succeeds
- [x] \`npx nx run twenty-sdk:typecheck\` passes
- [x] \`npx nx run twenty-sdk:test:unit\` passes (31 files / 257 tests)
- [x] \`npx nx run-many -t typecheck
--projects=twenty-front,twenty-server,twenty-front-component-renderer,twenty-sdk,twenty-shared,bundle-investigation\`
passes
- [x] \`node
packages/twenty-apps/internal/bundle-investigation/scripts/build-variants.mjs\`
produces the sizes above
- [ ] CI green
Made with [Cursor](https://cursor.com)
|
||
|
|
619ea13649 |
Twenty for twenty app (#19804)
## Twenty for Twenty: Resend module Introduces `packages/twenty-apps/internal/twenty-for-twenty`, the official internal Twenty app, with a first module integrating [Resend](https://resend.com). ### Breakdown **Resend module** (`src/modules/resend/`) - Two app variables: `RESEND_API_KEY` and `RESEND_WEBHOOK_SECRET`. - **Objects**: `resendContact`, `resendSegment`, `resendTemplate`, `resendBroadcast`, `resendEmail`, with relations between them and to standard `person`. - **Inbound sync (Resend → Twenty)**: - Cron-driven logic function `sync-resend-data` (every 5 min) pulling all entities through paginated, rate-limit-aware utilities (`sync-contacts`, `sync-segments`, `sync-templates`, `sync-broadcasts`, `sync-emails`). - Webhook endpoint (`resend-webhook`) verifying signatures and handling `contact.*` and `email.*` events in real time. - `find-or-create-person` auto-links Resend contacts to Twenty people by email. - **Outbound sync (Twenty → Resend)**: DB-event logic functions for `contact.created/updated/deleted` and `segment.created/deleted`, with a `lastSyncedFromResend` field for loop prevention. - **UI**: views, page layouts, navigation menu items, and front components (`HtmlPreview`, `RecordHtmlViewer`) to preview email/template HTML in record pages; `sync-resend-data` command exposed as a front component. ### Setup See the new README for install steps, webhook configuration, and local testing with the Resend CLI. |
||
|
|
fb4d037b93 |
Upgrade self hosting application (#19680)
as title, installed on https://twentyfortwenty.twenty.com/objects/selfHostingUsers?viewId=20069db0-5137-4b2f-9b20-1797572b8eb8 |
||
|
|
884b06936e |
Switch app test infra to globalSetup with appDevOnce (#19623)
## Summary - Replace per-file `setupFiles` + manual `appBuild`/`appDeploy`/`appInstall` with vitest `globalSetup` that runs `appDevOnce` once for the entire suite and `appUninstall` in teardown - Add `fileParallelism: false` to prevent shared-state collisions between test files - Replace `app-install.integration-test.ts` with `schema.integration-test.ts` that verifies app installation, custom object schema (fields/relations), and CRUD - Add reusable test helpers (`client.ts`, `metadata.ts`, `mutations.ts`) - Applied to both `create-twenty-app` template and `postcard` example |
||
|
|
c26c0b9d71 |
Use app's own OAuth credentials for CoreApiClient generation (#19563)
## Summary - **SDK (`dev` & `dev --once`)**: After app registration, the CLI now obtains an `APPLICATION_ACCESS` token via `client_credentials` grant using the app's own `clientId`/`clientSecret`, and uses that token for CoreApiClient schema introspection — instead of the user's `config.accessToken` which returns the full unscoped schema. - **Config**: `oauthClientSecret` is now persisted alongside `oauthClientId` in `~/.twenty/config.json` when creating a new app registration, so subsequent `dev`/`dev --once` runs can obtain fresh app tokens without re-registration. - **CI action**: `spawn-twenty-app-dev-test` now outputs a proper `API_KEY` JWT (signed with the seeded dev workspace secret) instead of the previous hardcoded `ACCESS` token — giving consumers a real API key rather than a user session token. ## Motivation When developing Twenty apps, `yarn twenty dev` was using the CLI user's OAuth token for GraphQL schema introspection during CoreApiClient generation. This token (type `ACCESS`) has no `applicationId` claim, so the server returns the **full workspace schema** — including all objects — rather than the scoped schema the app should see at runtime (filtered by `applicationId`). This caused a discrepancy: the generated CoreApiClient contained fields the app couldn't actually query at runtime with its `APPLICATION_ACCESS` token. By switching to `client_credentials` grant, the SDK now introspects with the same token type the app will use in production, ensuring the generated client accurately reflects the app's runtime capabilities. |
||
|
|
aed81a54a2 |
Upgrade cli tool version in technical apps (#19542)
as title |
||
|
|
d2f51cc939 |
Fix pre post logic function not executed (#19462)
- removes pre-install function
- execute **asyncrhonously** post-install function at application
installation
- add optional `shouldRunOnVersionUpgrade` boolean value on post-install
function definition default false
- update PostInstallPayload to
```
export type PostInstallPayload = {
previousVersion?: string;
newVersion: string;
};
```
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
|
||
|
|
5eaabe95e7 |
Fix role synchronisation (#19469)
As title solves https://discord.com/channels/1130383047699738754/1491167098398052503 |
||
|
|
bc7b5aee58 |
chore: centralize deploy/install CD actions in twentyhq/twenty (#19454)
## Summary - Adds `deploy-twenty-app` and `install-twenty-app` composite actions to `.github/actions/` so app repos can reference them remotely — same pattern as `spawn-twenty-app-dev-test` for CI - Updates `cd.yml` in template, hello-world, and postcard to use `twentyhq/twenty/.github/actions/deploy-twenty-app@main` / `install-twenty-app@main` instead of local `./.github/actions/` copies - Removes the 6 local action files that were duplicated across template and example apps **Before** (each app repo carried its own action copies): ```yaml uses: ./.github/actions/deploy ``` **After** (centralized, like CI): ```yaml uses: twentyhq/twenty/.github/actions/deploy-twenty-app@main ``` Made with [Cursor](https://cursor.com) |
||
|
|
1ae88f4e4f |
chore: add CD workflow template and point spawn action to main (#19430)
## Summary - Adds reusable composite GitHub Actions for Twenty app deployment: - `.github/actions/deploy` — builds and deploys to a remote instance (`api-url`, `api-key` inputs) - `.github/actions/install` — installs/upgrades on a specific workspace (`api-url`, `api-key` inputs) - Adds a `cd.yml` CD workflow that calls both actions in sequence. The workflow: - Deploys on push to `main` - Can be triggered from a PR by adding a `deploy` label - Configures a named remote via `TWENTY_DEPLOY_URL` env var and `TWENTY_DEPLOY_API_KEY` secret - Applied to: `create-twenty-app` template, `postcard` example, `hello-world` example - Updates the `spawn-twenty-app-dev-test` action ref from `@feature/sdk-config-file-source-of-truth` to `@main` in all `ci.yml` files |
||
|
|
15eb3e7edc |
feat(sdk): use config file as single source of truth, remove env var fallbacks (#19409)
## Summary - **Config as source of truth**: `~/.twenty/config.json` is now the single source of truth for SDK authentication — env var fallbacks have been removed from the config resolution chain. - **Test instance support**: `twenty server start --test` spins up a dedicated Docker instance on port 2021 with its own config (`config.test.json`), so integration tests don't interfere with the dev environment. - **API key auth for marketplace**: Removed `UserAuthGuard` from `MarketplaceResolver` so API key tokens (workspace-scoped) can call `installMarketplaceApp`. - **CI for example apps**: Added monorepo CI workflows for `hello-world` and `postcard` example apps to catch regressions. - **Simplified CI**: All `ci-create-app-e2e` and example app workflows now use a shared `spawn-twenty-app-dev-test` action (Docker-based) instead of building the server from source. Consolidated auth env vars to `TWENTY_API_URL` + `TWENTY_API_KEY`. - **Template publishing fix**: `create-twenty-app` template now correctly preserves `.github/` and `.gitignore` through npm publish (stored without leading dot, renamed after copy). ## Test plan - [x] CI SDK (lint, typecheck, unit, integration, e2e) — all green - [x] CI Example App Hello World — green - [x] CI Example App Postcard — green - [x] CI Create App E2E minimal — green - [x] CI Front, CI Server, CI Shared — green |
||
|
|
8702300b07 |
App feedbacks fix option id required in apps (#19386)
fixes https://discord.com/channels/1130383047699738754/1488226371032453292 |
||
|
|
7bf309ba73 |
Update last interaction app (#19332)
Rewrite to 0.8.0 SDK |