Compare commits

..
Author SHA1 Message Date
Charles BochetandCursor d66c6456c4 fix: distinguish soft-deleted relations from RLS-restricted ones
When a relation FK exists but the target record is null, the frontend
now checks whether row-level permission predicates are configured on
the target object. If RLS is active, shows "Not shared" (record may be
access-restricted). If no RLS exists, shows empty (record is
soft-deleted).

Fixes #20076

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 23:33:37 +02:00
1210 changed files with 26593 additions and 29131 deletions
+31 -32
View File
@@ -106,35 +106,34 @@ Replace `{VERSION}` with the actual version number (e.g., `1.9.0`)
### 2. Create File Structure
**Create changelog file:**
- Path: `packages/twenty-website-new/src/content/releases/{VERSION}.mdx`
- Example: `packages/twenty-website-new/src/content/releases/1.9.0.mdx`
- Path: `packages/twenty-website/src/content/releases/{VERSION}.mdx`
- Example: `packages/twenty-website/src/content/releases/1.9.0.mdx`
**Create image folder:**
- Path: `packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/`
- Example for version 1.9.0: `packages/twenty-website-new/public/images/releases/1.9/`
- Example for version 2.0.0: `packages/twenty-website-new/public/images/releases/2.0/`
- Path: `packages/twenty-website/public/images/releases/{MINOR_VERSION}/`
- Example for version 1.9.0: `packages/twenty-website/public/images/releases/1.9/`
- Example for version 2.0.0: `packages/twenty-website/public/images/releases/2.0/`
```bash
# Create the image folder
mkdir -p packages/twenty-website-new/public/images/releases/{MINOR_VERSION}
mkdir -p packages/twenty-website/public/images/releases/{MINOR_VERSION}
```
### 3. Move Illustration Files
**Source:** `/Users/thomascolasdesfrancs/Downloads/🆕`
**Destination:** `packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/`
**Destination:** `packages/twenty-website/public/images/releases/{MINOR_VERSION}/`
**Naming Convention:** `{VERSION}-descriptive-name.webp`
**Naming Convention:** `{VERSION}-descriptive-name.png`
Examples:
- `1.9.0-feature-name.webp`
- `1.9.0-another-feature.webp`
- `1.9.0-feature-name.png`
- `1.9.0-another-feature.png`
```bash
# Move and rename source files, then convert to webp if needed
cp ~/Downloads/🆕/source-file.png packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-name.png
cd packages/twenty-website-new && node scripts/convert-png-to-webp.mjs
# Move and rename files
cp ~/Downloads/🆕/source-file.png packages/twenty-website/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-name.png
```
### 4. Research Features (if needed)
@@ -159,19 +158,19 @@ Date: {YYYY-MM-DD}
Short description explaining what the feature does and why it's useful. Keep it user-focused and concise (1-2 sentences).
![](/images/releases/{MINOR_VERSION}/{VERSION}-feature-1.webp)
![](/images/releases/{MINOR_VERSION}/{VERSION}-feature-1.png)
# Feature 2 Name
Another short description of the second feature.
![](/images/releases/{MINOR_VERSION}/{VERSION}-feature-2.webp)
![](/images/releases/{MINOR_VERSION}/{VERSION}-feature-2.png)
# Feature 3 Name
Description of the third feature.
![](/images/releases/{MINOR_VERSION}/{VERSION}-feature-3.webp)
![](/images/releases/{MINOR_VERSION}/{VERSION}-feature-3.png)
```
**Style Guidelines:**
@@ -183,7 +182,7 @@ Description of the third feature.
- **NEVER mention the brand name "Twenty"** in changelog text - use "your workspace", "the platform", or similar neutral references instead
**Reference Previous Changelogs:**
- Check `packages/twenty-website-new/src/content/releases/` for examples
- Check `packages/twenty-website/src/content/releases/` for examples
- Recent releases: 1.7.0.mdx, 1.6.0.mdx, 1.5.0.mdx
### 6. Review
@@ -191,10 +190,10 @@ Description of the third feature.
Open the changelog file for review:
```bash
# Open in Cursor
cursor packages/twenty-website-new/src/content/releases/{VERSION}.mdx
cursor packages/twenty-website/src/content/releases/{VERSION}.mdx
# Open image folder to verify illustrations
open packages/twenty-website-new/public/images/releases/{MINOR_VERSION}
open packages/twenty-website/public/images/releases/{MINOR_VERSION}
```
Review checklist:
@@ -222,8 +221,8 @@ I've created the changelog for version {VERSION}. Here's the content for your re
[Show full MDX content]
Images moved to:
- packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-1.webp
- packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-2.webp
- packages/twenty-website/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-1.png
- packages/twenty-website/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-2.png
Please review the content. Once you approve, I'll commit the changes and create the pull request.
```
@@ -242,8 +241,8 @@ Possible user responses:
git status
# Add files
git add packages/twenty-website-new/src/content/releases/{VERSION}.mdx
git add packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/
git add packages/twenty-website/src/content/releases/{VERSION}.mdx
git add packages/twenty-website/public/images/releases/{MINOR_VERSION}/
# Commit
git commit -m "Add {VERSION} release changelog"
@@ -266,7 +265,7 @@ This release includes:
- Feature 2
- Feature 3
Changelog file: \`packages/twenty-website-new/src/content/releases/{VERSION}.mdx\`
Changelog file: \`packages/twenty-website/src/content/releases/{VERSION}.mdx\`
Release date: {DATE}" \
--base main \
--head {VERSION}
@@ -280,21 +279,21 @@ Or visit: `https://github.com/twentyhq/twenty/pull/new/{VERSION}`
- **Format**: `{MAJOR}.{MINOR}.{PATCH}.mdx`
- **Convention**: One file per complete version
- **Examples**: `1.6.0.mdx`, `1.7.0.mdx`, `2.0.0.mdx`
- **Location**: `packages/twenty-website-new/src/content/releases/`
- **Location**: `packages/twenty-website/src/content/releases/`
### Image Folders
- **Format**: `{MAJOR}.{MINOR}/`
- **Convention**: One folder per minor version (shared across patches)
- **Examples**: `1.6/`, `1.7/`, `2.0/`
- **Location**: `packages/twenty-website-new/public/images/releases/`
- **Location**: `packages/twenty-website/public/images/releases/`
### Image Files
- **Format**: `{VERSION}-descriptive-name.webp`
- **Format**: `{VERSION}-descriptive-name.png`
- **Convention**: Kebab-case descriptive names
- **Examples**:
- `1.8.0-workflow-iterator.webp`
- `1.8.0-bulk-select.webp`
- `1.9.0-new-feature.webp`
- `1.8.0-workflow-iterator.png`
- `1.8.0-bulk-select.png`
- `1.9.0-new-feature.png`
## Quick Reference Template
@@ -311,8 +310,8 @@ Features to document:
3. ___________________________
Branch name: {VERSION}
Changelog path: packages/twenty-website-new/src/content/releases/{VERSION}.mdx
Images path: packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/
Changelog path: packages/twenty-website/src/content/releases/{VERSION}.mdx
Images path: packages/twenty-website/public/images/releases/{MINOR_VERSION}/
```
## Tips
+38 -25
View File
@@ -1,12 +1,12 @@
name: CI Website
permissions:
contents: read
on:
pull_request:
merge_group:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
@@ -18,40 +18,53 @@ jobs:
with:
files: |
package.json
yarn.lock
packages/twenty-website-new/**
packages/twenty-shared/**
website-task:
packages/twenty-website/**
website-build:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
timeout-minutes: 10
runs-on: ubuntu-latest
env:
NODE_OPTIONS: '--max-old-space-size=6144'
strategy:
matrix:
task: [lint, typecheck, test]
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Run ${{ matrix.task }} task
uses: ./.github/actions/nx-affected
with:
tag: scope:website
tasks: ${{ matrix.task }}
- name: Server / Create DB
run: PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
- name: Website / Run migrations
run: npx nx database:migrate twenty-website
env:
DATABASE_PG_URL: postgres://postgres:postgres@localhost:5432/default
- name: Website / Build Website
run: npx nx build twenty-website
env:
DATABASE_PG_URL: postgres://postgres:postgres@localhost:5432/default
KEYSTATIC_GITHUB_CLIENT_ID: xxx
KEYSTATIC_GITHUB_CLIENT_SECRET: xxx
KEYSTATIC_SECRET: xxx
NEXT_PUBLIC_KEYSTATIC_GITHUB_APP_SLUG: xxx
ci-website-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, website-task]
needs: [changed-files-check, website-build]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
+1 -2
View File
@@ -110,8 +110,7 @@ packages/
├── twenty-ui/ # Shared UI components library
├── twenty-shared/ # Common types and utilities
├── twenty-emails/ # Email templates with React Email
├── twenty-website-new/ # Next.js marketing website
├── twenty-docs/ # Documentation website
├── twenty-website/ # Next.js documentation website
├── twenty-zapier/ # Zapier integration
└── twenty-e2e-testing/ # Playwright E2E tests
```
+43 -43
View File
@@ -1,19 +1,19 @@
<p align="center">
<a href="https://www.twenty.com">
<img src="./packages/twenty-website-new/public/images/core/logo.svg" width="100px" alt="Twenty logo" />
<img src="./packages/twenty-website/public/images/core/logo.svg" width="100px" alt="Twenty logo" />
</a>
</p>
<h2 align="center" >The #1 Open-Source CRM</h2>
<p align="center"><a href="https://twenty.com"><img src="./packages/twenty-website-new/public/images/readme/globe-icon.svg" width="12" height="12"/> Website</a> · <a href="https://docs.twenty.com"><img src="./packages/twenty-website-new/public/images/readme/book-icon.svg" width="12" height="12"/> Documentation</a> · <a href="https://github.com/orgs/twentyhq/projects/1"><img src="./packages/twenty-website-new/public/images/readme/map-icon.svg" width="12" height="12"/> Roadmap </a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website-new/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty"><img src="./packages/twenty-website-new/public/images/readme/figma-icon.png" width="12" height="12"/> Figma</a></p>
<p align="center"><a href="https://twenty.com"><img src="./packages/twenty-website/public/images/readme/globe-icon.svg" width="12" height="12"/> Website</a> · <a href="https://docs.twenty.com"><img src="./packages/twenty-website/public/images/readme/book-icon.svg" width="12" height="12"/> Documentation</a> · <a href="https://github.com/orgs/twentyhq/projects/1"><img src="./packages/twenty-website/public/images/readme/map-icon.svg" width="12" height="12"/> Roadmap </a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty"><img src="./packages/twenty-website/public/images/readme/figma-icon.png" width="12" height="12"/> Figma</a></p>
<p align="center">
<a href="https://www.twenty.com">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/github-cover-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/github-cover-light.png" />
<img src="./packages/twenty-website-new/public/images/readme/github-cover-light.png" alt="Twenty banner" />
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/github-cover-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/github-cover-light.png" />
<img src="./packages/twenty-website/public/images/readme/github-cover-light.png" alt="Twenty banner" />
</picture>
</a>
</p>
@@ -24,17 +24,17 @@
Twenty gives technical teams the building blocks for a custom CRM that meets complex business needs and quickly adapts as the business evolves. Twenty is the CRM you build, ship, and version like the rest of your stack.
<a href="https://twenty.com/why-twenty"><img src="./packages/twenty-website-new/public/images/readme/star-icon.svg" width="14" height="14"/> Learn more about why we built Twenty</a>
<a href="https://twenty.com/why-twenty"><img src="./packages/twenty-website/public/images/readme/star-icon.svg" width="14" height="14"/> Learn more about why we built Twenty</a>
<br />
# Installation
### <img src="./packages/twenty-website-new/public/images/readme/globe-icon.svg" width="14" height="14"/> Cloud
### <img src="./packages/twenty-website/public/images/readme/globe-icon.svg" width="14" height="14"/> Cloud
The fastest way to get started. Sign up at [twenty.com](https://twenty.com) and spin up a workspace in under a minute, with no infrastructure to manage and always up to date.
### <img src="./packages/twenty-website-new/public/images/readme/book-icon.svg" width="14" height="14"/> Build an app
### <img src="./packages/twenty-website/public/images/readme/book-icon.svg" width="14" height="14"/> Build an app
Scaffold a new app with the Twenty CLI:
@@ -68,7 +68,7 @@ npx twenty deploy
See the [app development guide](https://docs.twenty.com/developers/extend/apps/getting-started) for objects, views, agents, and logic functions.
### <img src="./packages/twenty-website-new/public/images/readme/rocket-icon.svg" width="14" height="14"/> Self-hosting
### <img src="./packages/twenty-website/public/images/readme/rocket-icon.svg" width="14" height="14"/> Self-hosting
Run Twenty on your own infrastructure with [Docker Compose](https://docs.twenty.com/developers/self-host/capabilities/docker-compose), or contribute locally via the [local setup guide](https://docs.twenty.com/developers/contribute/capabilities/local-setup).
@@ -79,61 +79,61 @@ Run Twenty on your own infrastructure with [Docker Compose](https://docs.twenty.
Twenty gives you the building blocks of a modern CRM (objects, views, workflows, and agents) and lets you extend them as code. Here's a tour of what's in the box.
Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduction"><img src="./packages/twenty-website-new/public/images/readme/planner-icon.svg" width="14" height="14"/> User Guide</a> for product walkthroughs, or the <a href="https://docs.twenty.com"><img src="./packages/twenty-website-new/public/images/readme/book-icon.svg" width="14" height="14"/> Documentation</a> for developer reference.
Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduction"><img src="./packages/twenty-website/public/images/readme/planner-icon.svg" width="14" height="14"/> User Guide</a> for product walkthroughs, or the <a href="https://docs.twenty.com"><img src="./packages/twenty-website/public/images/readme/book-icon.svg" width="14" height="14"/> Documentation</a> for developer reference.
<table align="center">
<tr>
<td width="50%">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/v2-build-apps-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/v2-build-apps-light.png" />
<img src="./packages/twenty-website-new/public/images/readme/v2-build-apps-light.png" alt="Create your apps" />
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-build-apps-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-build-apps-light.png" />
<img src="./packages/twenty-website/public/images/readme/v2-build-apps-light.png" alt="Create your apps" />
</picture>
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/getting-started"><img src="./packages/twenty-website-new/public/images/readme/code-icon.svg" width="16" height="16"/> Learn more about apps in doc</a></p>
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/getting-started"><img src="./packages/twenty-website/public/images/readme/code-icon.svg" width="16" height="16"/> Learn more about apps in doc</a></p>
</td>
<td width="50%">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/v2-version-control-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/v2-version-control-light.png" />
<img src="./packages/twenty-website-new/public/images/readme/v2-version-control-light.png" alt="Stay on top with version control" />
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-version-control-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-version-control-light.png" />
<img src="./packages/twenty-website/public/images/readme/v2-version-control-light.png" alt="Stay on top with version control" />
</picture>
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/publishing"><img src="./packages/twenty-website-new/public/images/readme/monitor-icon.svg" width="16" height="16"/> Learn more about version control in doc</a></p>
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/publishing"><img src="./packages/twenty-website/public/images/readme/monitor-icon.svg" width="16" height="16"/> Learn more about version control in doc</a></p>
</td>
</tr>
<tr>
<td width="50%">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/v2-all-tools-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/v2-all-tools-light.png" />
<img src="./packages/twenty-website-new/public/images/readme/v2-all-tools-light.png" alt="All the tools you need to build anything" />
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-all-tools-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-all-tools-light.png" />
<img src="./packages/twenty-website/public/images/readme/v2-all-tools-light.png" alt="All the tools you need to build anything" />
</picture>
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/building"><img src="./packages/twenty-website-new/public/images/readme/rocket-icon.svg" width="16" height="16"/> Learn more about primitives in doc</a></p>
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/building"><img src="./packages/twenty-website/public/images/readme/rocket-icon.svg" width="16" height="16"/> Learn more about primitives in doc</a></p>
</td>
<td width="50%">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/v2-tools-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/v2-tools-light.png" />
<img src="./packages/twenty-website-new/public/images/readme/v2-tools-light.png" alt="Customize your layouts" />
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-tools-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-tools-light.png" />
<img src="./packages/twenty-website/public/images/readme/v2-tools-light.png" alt="Customize your layouts" />
</picture>
<p align="center"><a href="https://docs.twenty.com/user-guide/layout/overview"><img src="./packages/twenty-website-new/public/images/readme/planner-icon.svg" width="16" height="16"/> Learn more about layouts in doc</a></p>
<p align="center"><a href="https://docs.twenty.com/user-guide/layout/overview"><img src="./packages/twenty-website/public/images/readme/planner-icon.svg" width="16" height="16"/> Learn more about layouts in doc</a></p>
</td>
</tr>
<tr>
<td width="50%">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/v2-ai-agents-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/v2-ai-agents-light.png" />
<img src="./packages/twenty-website-new/public/images/readme/v2-ai-agents-light.png" alt="AI agents and chats" />
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-ai-agents-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-ai-agents-light.png" />
<img src="./packages/twenty-website/public/images/readme/v2-ai-agents-light.png" alt="AI agents and chats" />
</picture>
<p align="center"><a href="https://docs.twenty.com/user-guide/ai/overview"><img src="./packages/twenty-website-new/public/images/readme/message-icon.svg" width="16" height="16"/> Learn more about AI in doc</a></p>
<p align="center"><a href="https://docs.twenty.com/user-guide/ai/overview"><img src="./packages/twenty-website/public/images/readme/message-icon.svg" width="16" height="16"/> Learn more about AI in doc</a></p>
</td>
<td width="50%">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/v2-crm-tools-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/v2-crm-tools-light.png" />
<img src="./packages/twenty-website-new/public/images/readme/v2-crm-tools-light.png" alt="Plus all the tools of a good CRM" />
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-crm-tools-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-crm-tools-light.png" />
<img src="./packages/twenty-website/public/images/readme/v2-crm-tools-light.png" alt="Plus all the tools of a good CRM" />
</picture>
<p align="center"><a href="https://docs.twenty.com/user-guide/introduction"><img src="./packages/twenty-website-new/public/images/readme/star-icon.svg" width="16" height="16"/> Learn more about CRM features in doc</a></p>
<p align="center"><a href="https://docs.twenty.com/user-guide/introduction"><img src="./packages/twenty-website/public/images/readme/star-icon.svg" width="16" height="16"/> Learn more about CRM features in doc</a></p>
</td>
</tr>
</table>
@@ -142,23 +142,23 @@ Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduc
# Stack
- <a href="https://www.typescriptlang.org/"><img src="./packages/twenty-website-new/public/images/readme/stack-typescript.svg" width="14" height="14"/> TypeScript</a>
- <a href="https://nx.dev/"><img src="./packages/twenty-website-new/public/images/readme/stack-nx.svg" width="14" height="14"/> Nx</a>
- <a href="https://nestjs.com/"><img src="./packages/twenty-website-new/public/images/readme/stack-nestjs.svg" width="14" height="14"/> NestJS</a>, with <a href="https://bullmq.io/">BullMQ</a>, <a href="https://www.postgresql.org/"><img src="./packages/twenty-website-new/public/images/readme/stack-postgresql.svg" width="14" height="14"/> PostgreSQL</a>, <a href="https://redis.io/"><img src="./packages/twenty-website-new/public/images/readme/stack-redis.svg" width="14" height="14"/> Redis</a>
- <a href="https://reactjs.org/"><img src="./packages/twenty-website-new/public/images/readme/stack-react.svg" width="14" height="14"/> React</a>, with <a href="https://jotai.org/">Jotai</a>, <a href="https://linaria.dev/">Linaria</a> and <a href="https://lingui.dev/">Lingui</a>
- <a href="https://www.typescriptlang.org/"><img src="./packages/twenty-website/public/images/readme/stack-typescript.svg" width="14" height="14"/> TypeScript</a>
- <a href="https://nx.dev/"><img src="./packages/twenty-website/public/images/readme/stack-nx.svg" width="14" height="14"/> Nx</a>
- <a href="https://nestjs.com/"><img src="./packages/twenty-website/public/images/readme/stack-nestjs.svg" width="14" height="14"/> NestJS</a>, with <a href="https://bullmq.io/">BullMQ</a>, <a href="https://www.postgresql.org/"><img src="./packages/twenty-website/public/images/readme/stack-postgresql.svg" width="14" height="14"/> PostgreSQL</a>, <a href="https://redis.io/"><img src="./packages/twenty-website/public/images/readme/stack-redis.svg" width="14" height="14"/> Redis</a>
- <a href="https://reactjs.org/"><img src="./packages/twenty-website/public/images/readme/stack-react.svg" width="14" height="14"/> React</a>, with <a href="https://jotai.org/">Jotai</a>, <a href="https://linaria.dev/">Linaria</a> and <a href="https://lingui.dev/">Lingui</a>
# Thanks
<p align="center">
<a href="https://www.chromatic.com/"><img src="./packages/twenty-website-new/public/images/readme/chromatic.png" height="28" alt="Chromatic" /></a>
<a href="https://www.chromatic.com/"><img src="./packages/twenty-website/public/images/readme/chromatic.png" height="28" alt="Chromatic" /></a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://greptile.com"><img src="./packages/twenty-website-new/public/images/readme/greptile.png" height="28" alt="Greptile" /></a>
<a href="https://greptile.com"><img src="./packages/twenty-website/public/images/readme/greptile.png" height="28" alt="Greptile" /></a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://sentry.io/"><img src="./packages/twenty-website-new/public/images/readme/sentry.png" height="28" alt="Sentry" /></a>
<a href="https://sentry.io/"><img src="./packages/twenty-website/public/images/readme/sentry.png" height="28" alt="Sentry" /></a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://crowdin.com/"><img src="./packages/twenty-website-new/public/images/readme/crowdin.png" height="28" alt="Crowdin" /></a>
<a href="https://crowdin.com/"><img src="./packages/twenty-website/public/images/readme/crowdin.png" height="28" alt="Crowdin" /></a>
</p>
Thanks to these amazing services that we use and recommend for UI testing (Chromatic), code review (Greptile), catching bugs (Sentry) and translating (Crowdin).
@@ -166,4 +166,4 @@ Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduc
# Join the Community
<p><a href="https://github.com/twentyhq/twenty"><img src="./packages/twenty-website-new/public/images/readme/star-icon.svg" width="12" height="12"/> Star the repo</a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website-new/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://github.com/twentyhq/twenty/discussions"><img src="./packages/twenty-website-new/public/images/readme/message-icon.svg" width="12" height="12"/> Feature requests</a> · <a href="https://github.com/orgs/twentyhq/projects/1/views/35"><img src="./packages/twenty-website-new/public/images/readme/rocket-icon.svg" width="12" height="12"/> Releases</a> · <a href="https://twitter.com/twentycrm"><img src="./packages/twenty-website-new/public/images/readme/x-icon.svg" width="12" height="12"/> X</a> · <a href="https://www.linkedin.com/company/twenty/"><img src="./packages/twenty-website-new/public/images/readme/linkedin-icon.svg" width="12" height="12"/> LinkedIn</a> · <a href="https://twenty.crowdin.com/twenty"><img src="./packages/twenty-website-new/public/images/readme/language-icon.svg" width="12" height="12"/> Crowdin</a> · <a href="https://github.com/twentyhq/twenty/contribute"><img src="./packages/twenty-website-new/public/images/readme/code-icon.svg" width="12" height="12"/> Contribute</a></p>
<p><a href="https://github.com/twentyhq/twenty"><img src="./packages/twenty-website/public/images/readme/star-icon.svg" width="12" height="12"/> Star the repo</a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://github.com/twentyhq/twenty/discussions"><img src="./packages/twenty-website/public/images/readme/message-icon.svg" width="12" height="12"/> Feature requests</a> · <a href="https://github.com/orgs/twentyhq/projects/1/views/35"><img src="./packages/twenty-website/public/images/readme/rocket-icon.svg" width="12" height="12"/> Releases</a> · <a href="https://twitter.com/twentycrm"><img src="./packages/twenty-website/public/images/readme/x-icon.svg" width="12" height="12"/> X</a> · <a href="https://www.linkedin.com/company/twenty/"><img src="./packages/twenty-website/public/images/readme/linkedin-icon.svg" width="12" height="12"/> LinkedIn</a> · <a href="https://twenty.crowdin.com/twenty"><img src="./packages/twenty-website/public/images/readme/language-icon.svg" width="12" height="12"/> Crowdin</a> · <a href="https://github.com/twentyhq/twenty/contribute"><img src="./packages/twenty-website/public/images/readme/code-icon.svg" width="12" height="12"/> Contribute</a></p>
+2 -3
View File
@@ -9,7 +9,6 @@
"@nx/web": "22.5.4",
"@types/react": "^18.2.39",
"@types/react-dom": "^18.2.15",
"@yarnpkg/types": "^4.0.0",
"concurrently": "^8.2.2",
"http-server": "^14.1.1",
"nx": "22.5.4",
@@ -34,8 +33,7 @@
"@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",
"@opentelemetry/api": "1.9.1",
"chokidar": "^3.6.0"
"@opentelemetry/api": "1.9.1"
},
"version": "0.2.1",
"nx": {},
@@ -53,6 +51,7 @@
"packages/twenty-ui",
"packages/twenty-utils",
"packages/twenty-zapier",
"packages/twenty-website",
"packages/twenty-website-new",
"packages/twenty-docs",
"packages/twenty-e2e-testing",
+1 -1
View File
@@ -1,7 +1,7 @@
<div align="center">
<a href="https://twenty.com">
<picture>
<img alt="Twenty logo" src="https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-website-new/public/images/core/logo.svg" height="128">
<img alt="Twenty logo" src="https://raw.githubusercontent.com/twentyhq/twenty/2f25922f4cd5bd61e1427c57c4f8ea224e1d552c/packages/twenty-website/public/images/core/logo.svg" height="128">
</picture>
</a>
<h1>Create Twenty App</h1>
@@ -19,8 +19,8 @@
"test:watch": "vitest"
},
"dependencies": {
"twenty-client-sdk": "2.2.0",
"twenty-sdk": "2.2.0"
"twenty-client-sdk": "0.9.0",
"twenty-sdk": "0.9.0"
},
"devDependencies": {
"@types/node": "^24.7.2",
@@ -1,12 +1,8 @@
import { useEffect } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
enqueueSnackbar,
unmountFrontComponent,
updateProgress,
useRecordId,
} from 'twenty-sdk/front-component';
import { useRecordId, updateProgress, enqueueSnackbar, unmountFrontComponent } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { isDefined } from 'twenty-shared/utils';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
const SYSTEM_PROMPT =
@@ -18,9 +14,9 @@ const GeneratePostCardEffect = () => {
const recordId = useRecordId();
useEffect(() => {
if (recordId === null) {
if (!isDefined(recordId)) {
enqueueSnackbar({
message: 'Please select exactly one record',
message: 'No record selected',
variant: 'error',
});
unmountFrontComponent();
@@ -1,12 +1,8 @@
import { useEffect } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
enqueueSnackbar,
unmountFrontComponent,
updateProgress,
useRecordId,
} from 'twenty-sdk/front-component';
import { useRecordId, updateProgress, enqueueSnackbar, unmountFrontComponent } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { isDefined } from 'twenty-shared/utils';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
const SendPostCardsEffect = () => {
@@ -18,25 +14,55 @@ const SendPostCardsEffect = () => {
await updateProgress(0.1);
const client = new CoreApiClient();
let idsToSend: string[] = [];
if (isDefined(recordId)) {
idsToSend = [recordId];
} else {
const { postCards } = await client.query({
postCards: {
__args: {
filter: { status: { eq: 'DRAFT' } },
},
edges: { node: { id: true } },
},
});
idsToSend =
postCards?.edges?.map(
(edge: { node: { id: string; status: true } }) => edge.node.id,
) ?? [];
}
if (idsToSend.length === 0) {
await updateProgress(1);
await unmountFrontComponent();
return;
}
await updateProgress(0.3);
if (recordId) {
for (let i = 0; i < idsToSend.length; i++) {
await client.mutation({
updatePostCard: {
__args: {
id: recordId,
id: idsToSend[i],
data: { status: 'SENT' },
},
id: true,
},
});
await enqueueSnackbar({
message: `Postcard sent`,
variant: 'success',
});
await updateProgress(0.3 + (0.7 * (i + 1)) / idsToSend.length);
}
const count = idsToSend.length;
await enqueueSnackbar({
message: `${count} postcard${count > 1 ? 's' : ''} sent`,
variant: 'success',
});
await unmountFrontComponent();
} catch (error) {
const message =
@@ -3317,8 +3317,8 @@ __metadata:
oxlint: "npm:^0.16.0"
react: "npm:^19.0.0"
react-dom: "npm:^19.0.0"
twenty-client-sdk: "npm:2.2.0"
twenty-sdk: "npm:2.2.0"
twenty-client-sdk: "npm:0.9.0"
twenty-sdk: "npm:0.9.0"
typescript: "npm:^5.9.3"
vite-tsconfig-paths: "npm:^4.2.1"
vitest: "npm:^3.1.1"
@@ -4059,21 +4059,21 @@ __metadata:
languageName: node
linkType: hard
"twenty-client-sdk@npm:2.2.0":
version: 2.2.0
resolution: "twenty-client-sdk@npm:2.2.0"
"twenty-client-sdk@npm:0.9.0":
version: 0.9.0
resolution: "twenty-client-sdk@npm:0.9.0"
dependencies:
"@genql/cli": "npm:^3.0.3"
"@genql/runtime": "npm:^2.10.0"
esbuild: "npm:^0.25.0"
graphql: "npm:^16.8.1"
checksum: 10c0/90122593efa53440ae386960211a2c274b13a410942bf6f9bc35cf952ae55fe83280e29074677fd284fa3c79069fc578980db06a92a143c08e043f865dbbbd3c
checksum: 10c0/4b42a6622a9852fc3eca50c1131b116c5602af006ee5f1d3b4f1e97721bbc8ef5c2f3b60d9dee3ca9ca8c3c7ad4b292a7b55007b779b4c042997dbc29940ba54
languageName: node
linkType: hard
"twenty-sdk@npm:2.2.0":
version: 2.2.0
resolution: "twenty-sdk@npm:2.2.0"
"twenty-sdk@npm:0.9.0":
version: 0.9.0
resolution: "twenty-sdk@npm:0.9.0"
dependencies:
"@genql/cli": "npm:^3.0.3"
"@genql/runtime": "npm:^2.10.0"
@@ -4093,7 +4093,7 @@ __metadata:
react: "npm:^19.0.0"
react-dom: "npm:^19.0.0"
tinyglobby: "npm:^0.2.15"
twenty-client-sdk: "npm:2.2.0"
twenty-client-sdk: "npm:0.9.0"
typescript: "npm:^5.9.2"
uuid: "npm:^13.0.0"
vite: "npm:^7.0.0"
@@ -4101,7 +4101,7 @@ __metadata:
zod: "npm:^4.1.11"
bin:
twenty: dist/cli.cjs
checksum: 10c0/8978b4b0aa5ea282c8f76799347d9d1812901ec609bc2bd9270261a6bc5589ad361f6102a06900a4511a29a8378cd3811c2c0bad9f01cb8adadabca6d96dfd0b
checksum: 10c0/27f93e5edac3265f819abacc853598435718020258a95d55518562e086e42daabae784964b42005d8e4ff30d1d6f13a12a38c656e18c60bad98da3f579a8e1c3
languageName: node
linkType: hard
@@ -10,13 +10,5 @@ export default defineLogicFunction({
description: 'Look up a recipient by name to find their details',
timeoutSeconds: 5,
handler,
toolTriggerSettings: {
inputSchema: {
type: 'object',
properties: {
recipientName: { type: 'string' },
},
required: ['recipientName'],
},
},
isTool: true,
});
@@ -111,8 +111,7 @@ export default defineLogicFunction({
description:
'Structured web search powered by Exa. Returns entity-aware results with category filtering (companies, people, research papers, news, and other content types). Prefer this when the query benefits from structured data or a specific category. For general real-time web browsing, prefer the native `web_search` tool when it is available.',
timeoutSeconds: 30,
toolTriggerSettings: {
inputSchema: exaWebSearchInputSchema,
},
isTool: true,
toolInputSchema: exaWebSearchInputSchema,
handler,
});
@@ -10,25 +10,24 @@ export default defineLogicFunction({
'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,
toolTriggerSettings: {
inputSchema: {
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).',
},
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'],
},
required: ['teamId', 'title'],
},
});
@@ -10,10 +10,9 @@ export default defineLogicFunction({
"Returns the connected user's Linear teams. Useful for picking a teamId to pass to create-linear-issue.",
timeoutSeconds: 15,
handler: listLinearTeamsHandler,
toolTriggerSettings: {
inputSchema: {
type: 'object',
properties: {},
},
isTool: true,
toolInputSchema: {
type: 'object',
properties: {},
},
});
@@ -51,7 +51,6 @@ type ApplicationRegistration {
logoUrl: String
createdAt: DateTime!
updatedAt: DateTime!
isConfigured: Boolean!
}
enum ApplicationRegistrationSourceType {
@@ -325,115 +324,6 @@ type FrontComponent {
applicationTokenPair: ApplicationTokenPair
}
type CommandMenuItem {
id: UUID!
workflowVersionId: UUID
frontComponentId: UUID
frontComponent: FrontComponent
engineComponentKey: EngineComponentKey!
label: String!
icon: String
shortLabel: String
position: Float!
isPinned: Boolean!
availabilityType: CommandMenuItemAvailabilityType!
payload: CommandMenuItemPayload
hotKeys: [String!]
conditionalAvailabilityExpression: String
availabilityObjectMetadataId: UUID
pageLayoutId: UUID
universalIdentifier: UUID
applicationId: UUID
createdAt: DateTime!
updatedAt: DateTime!
}
enum EngineComponentKey {
NAVIGATE_TO_NEXT_RECORD
NAVIGATE_TO_PREVIOUS_RECORD
CREATE_NEW_RECORD
DELETE_RECORDS
RESTORE_RECORDS
DESTROY_RECORDS
ADD_TO_FAVORITES
REMOVE_FROM_FAVORITES
EXPORT_NOTE_TO_PDF
EXPORT_RECORDS
UPDATE_MULTIPLE_RECORDS
MERGE_MULTIPLE_RECORDS
IMPORT_RECORDS
EXPORT_VIEW
SEE_DELETED_RECORDS
CREATE_NEW_VIEW
HIDE_DELETED_RECORDS
EDIT_RECORD_PAGE_LAYOUT
EDIT_DASHBOARD_LAYOUT
SAVE_DASHBOARD_LAYOUT
CANCEL_DASHBOARD_LAYOUT
DUPLICATE_DASHBOARD
ACTIVATE_WORKFLOW
DEACTIVATE_WORKFLOW
DISCARD_DRAFT_WORKFLOW
TEST_WORKFLOW
SEE_ACTIVE_VERSION_WORKFLOW
SEE_RUNS_WORKFLOW
SEE_VERSIONS_WORKFLOW
ADD_NODE_WORKFLOW
TIDY_UP_WORKFLOW
DUPLICATE_WORKFLOW
SEE_VERSION_WORKFLOW_RUN
SEE_WORKFLOW_WORKFLOW_RUN
STOP_WORKFLOW_RUN
SEE_RUNS_WORKFLOW_VERSION
SEE_WORKFLOW_WORKFLOW_VERSION
USE_AS_DRAFT_WORKFLOW_VERSION
SEE_VERSIONS_WORKFLOW_VERSION
SEARCH_RECORDS
SEARCH_RECORDS_FALLBACK
ASK_AI
VIEW_PREVIOUS_AI_CHATS
NAVIGATION
TRIGGER_WORKFLOW_VERSION
FRONT_COMPONENT_RENDERER
REPLY_TO_EMAIL_THREAD
COMPOSE_EMAIL
GO_TO_PEOPLE
GO_TO_COMPANIES
GO_TO_DASHBOARDS
GO_TO_OPPORTUNITIES
GO_TO_SETTINGS
GO_TO_TASKS
GO_TO_NOTES
GO_TO_WORKFLOWS
GO_TO_RUNS
DELETE_SINGLE_RECORD
DELETE_MULTIPLE_RECORDS
RESTORE_SINGLE_RECORD
RESTORE_MULTIPLE_RECORDS
DESTROY_SINGLE_RECORD
DESTROY_MULTIPLE_RECORDS
EXPORT_FROM_RECORD_INDEX
EXPORT_FROM_RECORD_SHOW
EXPORT_MULTIPLE_RECORDS
}
enum CommandMenuItemAvailabilityType {
GLOBAL
GLOBAL_OBJECT_CONTEXT
RECORD_SELECTION
FALLBACK
}
union CommandMenuItemPayload = PathCommandMenuItemPayload | ObjectMetadataCommandMenuItemPayload
type PathCommandMenuItemPayload {
path: String!
}
type ObjectMetadataCommandMenuItemPayload {
objectMetadataItemId: UUID!
}
type LogicFunction {
id: UUID!
name: String!
@@ -442,11 +332,11 @@ type LogicFunction {
timeoutSeconds: Float!
sourceHandlerPath: String!
handlerName: String!
toolInputSchema: JSON
isTool: Boolean!
cronTriggerSettings: JSON
databaseEventTriggerSettings: JSON
httpRouteTriggerSettings: JSON
toolTriggerSettings: JSON
workflowActionTriggerSettings: JSON
applicationId: UUID
universalIdentifier: UUID
createdAt: DateTime!
@@ -705,7 +595,6 @@ type Application {
defaultLogicFunctionRole: Role
agents: [Agent!]!
frontComponents: [FrontComponent!]!
commandMenuItems: [CommandMenuItem!]!
logicFunctions: [LogicFunction!]!
objects: [Object!]!
applicationVariables: [ApplicationVariable!]!
@@ -2435,6 +2324,114 @@ type PostgresCredentials {
workspaceId: UUID!
}
type CommandMenuItem {
id: UUID!
workflowVersionId: UUID
frontComponentId: UUID
frontComponent: FrontComponent
engineComponentKey: EngineComponentKey!
label: String!
icon: String
shortLabel: String
position: Float!
isPinned: Boolean!
availabilityType: CommandMenuItemAvailabilityType!
payload: CommandMenuItemPayload
hotKeys: [String!]
conditionalAvailabilityExpression: String
availabilityObjectMetadataId: UUID
pageLayoutId: UUID
applicationId: UUID
createdAt: DateTime!
updatedAt: DateTime!
}
enum EngineComponentKey {
NAVIGATE_TO_NEXT_RECORD
NAVIGATE_TO_PREVIOUS_RECORD
CREATE_NEW_RECORD
DELETE_RECORDS
RESTORE_RECORDS
DESTROY_RECORDS
ADD_TO_FAVORITES
REMOVE_FROM_FAVORITES
EXPORT_NOTE_TO_PDF
EXPORT_RECORDS
UPDATE_MULTIPLE_RECORDS
MERGE_MULTIPLE_RECORDS
IMPORT_RECORDS
EXPORT_VIEW
SEE_DELETED_RECORDS
CREATE_NEW_VIEW
HIDE_DELETED_RECORDS
EDIT_RECORD_PAGE_LAYOUT
EDIT_DASHBOARD_LAYOUT
SAVE_DASHBOARD_LAYOUT
CANCEL_DASHBOARD_LAYOUT
DUPLICATE_DASHBOARD
ACTIVATE_WORKFLOW
DEACTIVATE_WORKFLOW
DISCARD_DRAFT_WORKFLOW
TEST_WORKFLOW
SEE_ACTIVE_VERSION_WORKFLOW
SEE_RUNS_WORKFLOW
SEE_VERSIONS_WORKFLOW
ADD_NODE_WORKFLOW
TIDY_UP_WORKFLOW
DUPLICATE_WORKFLOW
SEE_VERSION_WORKFLOW_RUN
SEE_WORKFLOW_WORKFLOW_RUN
STOP_WORKFLOW_RUN
SEE_RUNS_WORKFLOW_VERSION
SEE_WORKFLOW_WORKFLOW_VERSION
USE_AS_DRAFT_WORKFLOW_VERSION
SEE_VERSIONS_WORKFLOW_VERSION
SEARCH_RECORDS
SEARCH_RECORDS_FALLBACK
ASK_AI
VIEW_PREVIOUS_AI_CHATS
NAVIGATION
TRIGGER_WORKFLOW_VERSION
FRONT_COMPONENT_RENDERER
REPLY_TO_EMAIL_THREAD
COMPOSE_EMAIL
GO_TO_PEOPLE
GO_TO_COMPANIES
GO_TO_DASHBOARDS
GO_TO_OPPORTUNITIES
GO_TO_SETTINGS
GO_TO_TASKS
GO_TO_NOTES
GO_TO_WORKFLOWS
GO_TO_RUNS
DELETE_SINGLE_RECORD
DELETE_MULTIPLE_RECORDS
RESTORE_SINGLE_RECORD
RESTORE_MULTIPLE_RECORDS
DESTROY_SINGLE_RECORD
DESTROY_MULTIPLE_RECORDS
EXPORT_FROM_RECORD_INDEX
EXPORT_FROM_RECORD_SHOW
EXPORT_MULTIPLE_RECORDS
}
enum CommandMenuItemAvailabilityType {
GLOBAL
GLOBAL_OBJECT_CONTEXT
RECORD_SELECTION
FALLBACK
}
union CommandMenuItemPayload = PathCommandMenuItemPayload | ObjectMetadataCommandMenuItemPayload
type PathCommandMenuItemPayload {
path: String!
}
type ObjectMetadataCommandMenuItemPayload {
objectMetadataItemId: UUID!
}
type ToolIndexEntry {
name: String!
description: String!
@@ -2553,7 +2550,7 @@ type ConnectedAccountDTO {
connectionParameters: ImapSmtpCaldavConnectionParameters
lastSignedInAt: DateTime
userWorkspaceId: UUID!
connectionProviderId: UUID
applicationConnectionProviderId: UUID
applicationId: UUID
name: String
visibility: String!
@@ -2584,7 +2581,7 @@ type ConnectedAccountPublicDTO {
scopes: [String!]
lastSignedInAt: DateTime
userWorkspaceId: UUID!
connectionProviderId: UUID
applicationConnectionProviderId: UUID
applicationId: UUID
name: String
visibility: String!
@@ -2873,8 +2870,6 @@ enum AllMetadataName {
fieldPermission
frontComponent
webhook
applicationVariable
connectionProvider
}
type MinimalObjectMetadata {
@@ -3783,12 +3778,12 @@ input CreateLogicFunctionFromSourceInput {
name: String!
description: String
timeoutSeconds: Float
toolInputSchema: JSON
isTool: Boolean
source: JSON
cronTriggerSettings: JSON
databaseEventTriggerSettings: JSON
httpRouteTriggerSettings: JSON
toolTriggerSettings: JSON
workflowActionTriggerSettings: JSON
}
input ExecuteOneLogicFunctionInput {
@@ -3812,13 +3807,13 @@ input UpdateLogicFunctionFromSourceInputUpdates {
description: String
timeoutSeconds: Float
sourceHandlerCode: String
toolInputSchema: JSON
handlerName: String
sourceHandlerPath: String
isTool: Boolean
cronTriggerSettings: JSON
databaseEventTriggerSettings: JSON
httpRouteTriggerSettings: JSON
toolTriggerSettings: JSON
workflowActionTriggerSettings: JSON
}
input CreateCommandMenuItemInput {
@@ -55,7 +55,6 @@ export interface ApplicationRegistration {
logoUrl?: Scalars['String']
createdAt: Scalars['DateTime']
updatedAt: Scalars['DateTime']
isConfigured: Scalars['Boolean']
__typename: 'ApplicationRegistration'
}
@@ -279,46 +278,6 @@ export interface FrontComponent {
__typename: 'FrontComponent'
}
export interface CommandMenuItem {
id: Scalars['UUID']
workflowVersionId?: Scalars['UUID']
frontComponentId?: Scalars['UUID']
frontComponent?: FrontComponent
engineComponentKey: EngineComponentKey
label: Scalars['String']
icon?: Scalars['String']
shortLabel?: Scalars['String']
position: Scalars['Float']
isPinned: Scalars['Boolean']
availabilityType: CommandMenuItemAvailabilityType
payload?: CommandMenuItemPayload
hotKeys?: Scalars['String'][]
conditionalAvailabilityExpression?: Scalars['String']
availabilityObjectMetadataId?: Scalars['UUID']
pageLayoutId?: Scalars['UUID']
universalIdentifier?: Scalars['UUID']
applicationId?: Scalars['UUID']
createdAt: Scalars['DateTime']
updatedAt: Scalars['DateTime']
__typename: 'CommandMenuItem'
}
export type EngineComponentKey = 'NAVIGATE_TO_NEXT_RECORD' | 'NAVIGATE_TO_PREVIOUS_RECORD' | 'CREATE_NEW_RECORD' | 'DELETE_RECORDS' | 'RESTORE_RECORDS' | 'DESTROY_RECORDS' | 'ADD_TO_FAVORITES' | 'REMOVE_FROM_FAVORITES' | 'EXPORT_NOTE_TO_PDF' | 'EXPORT_RECORDS' | 'UPDATE_MULTIPLE_RECORDS' | 'MERGE_MULTIPLE_RECORDS' | 'IMPORT_RECORDS' | 'EXPORT_VIEW' | 'SEE_DELETED_RECORDS' | 'CREATE_NEW_VIEW' | 'HIDE_DELETED_RECORDS' | 'EDIT_RECORD_PAGE_LAYOUT' | 'EDIT_DASHBOARD_LAYOUT' | 'SAVE_DASHBOARD_LAYOUT' | 'CANCEL_DASHBOARD_LAYOUT' | 'DUPLICATE_DASHBOARD' | 'ACTIVATE_WORKFLOW' | 'DEACTIVATE_WORKFLOW' | 'DISCARD_DRAFT_WORKFLOW' | 'TEST_WORKFLOW' | 'SEE_ACTIVE_VERSION_WORKFLOW' | 'SEE_RUNS_WORKFLOW' | 'SEE_VERSIONS_WORKFLOW' | 'ADD_NODE_WORKFLOW' | 'TIDY_UP_WORKFLOW' | 'DUPLICATE_WORKFLOW' | 'SEE_VERSION_WORKFLOW_RUN' | 'SEE_WORKFLOW_WORKFLOW_RUN' | 'STOP_WORKFLOW_RUN' | 'SEE_RUNS_WORKFLOW_VERSION' | 'SEE_WORKFLOW_WORKFLOW_VERSION' | 'USE_AS_DRAFT_WORKFLOW_VERSION' | 'SEE_VERSIONS_WORKFLOW_VERSION' | 'SEARCH_RECORDS' | 'SEARCH_RECORDS_FALLBACK' | 'ASK_AI' | 'VIEW_PREVIOUS_AI_CHATS' | 'NAVIGATION' | 'TRIGGER_WORKFLOW_VERSION' | 'FRONT_COMPONENT_RENDERER' | 'REPLY_TO_EMAIL_THREAD' | 'COMPOSE_EMAIL' | 'GO_TO_PEOPLE' | 'GO_TO_COMPANIES' | 'GO_TO_DASHBOARDS' | 'GO_TO_OPPORTUNITIES' | 'GO_TO_SETTINGS' | 'GO_TO_TASKS' | 'GO_TO_NOTES' | 'GO_TO_WORKFLOWS' | 'GO_TO_RUNS' | 'DELETE_SINGLE_RECORD' | 'DELETE_MULTIPLE_RECORDS' | 'RESTORE_SINGLE_RECORD' | 'RESTORE_MULTIPLE_RECORDS' | 'DESTROY_SINGLE_RECORD' | 'DESTROY_MULTIPLE_RECORDS' | 'EXPORT_FROM_RECORD_INDEX' | 'EXPORT_FROM_RECORD_SHOW' | 'EXPORT_MULTIPLE_RECORDS'
export type CommandMenuItemAvailabilityType = 'GLOBAL' | 'GLOBAL_OBJECT_CONTEXT' | 'RECORD_SELECTION' | 'FALLBACK'
export type CommandMenuItemPayload = (PathCommandMenuItemPayload | ObjectMetadataCommandMenuItemPayload) & { __isUnion?: true }
export interface PathCommandMenuItemPayload {
path: Scalars['String']
__typename: 'PathCommandMenuItemPayload'
}
export interface ObjectMetadataCommandMenuItemPayload {
objectMetadataItemId: Scalars['UUID']
__typename: 'ObjectMetadataCommandMenuItemPayload'
}
export interface LogicFunction {
id: Scalars['UUID']
name: Scalars['String']
@@ -327,11 +286,11 @@ export interface LogicFunction {
timeoutSeconds: Scalars['Float']
sourceHandlerPath: Scalars['String']
handlerName: Scalars['String']
toolInputSchema?: Scalars['JSON']
isTool: Scalars['Boolean']
cronTriggerSettings?: Scalars['JSON']
databaseEventTriggerSettings?: Scalars['JSON']
httpRouteTriggerSettings?: Scalars['JSON']
toolTriggerSettings?: Scalars['JSON']
workflowActionTriggerSettings?: Scalars['JSON']
applicationId?: Scalars['UUID']
universalIdentifier?: Scalars['UUID']
createdAt: Scalars['DateTime']
@@ -470,7 +429,6 @@ export interface Application {
defaultLogicFunctionRole?: Role
agents: Agent[]
frontComponents: FrontComponent[]
commandMenuItems: CommandMenuItem[]
logicFunctions: LogicFunction[]
objects: Object[]
applicationVariables: ApplicationVariable[]
@@ -2112,6 +2070,45 @@ export interface PostgresCredentials {
__typename: 'PostgresCredentials'
}
export interface CommandMenuItem {
id: Scalars['UUID']
workflowVersionId?: Scalars['UUID']
frontComponentId?: Scalars['UUID']
frontComponent?: FrontComponent
engineComponentKey: EngineComponentKey
label: Scalars['String']
icon?: Scalars['String']
shortLabel?: Scalars['String']
position: Scalars['Float']
isPinned: Scalars['Boolean']
availabilityType: CommandMenuItemAvailabilityType
payload?: CommandMenuItemPayload
hotKeys?: Scalars['String'][]
conditionalAvailabilityExpression?: Scalars['String']
availabilityObjectMetadataId?: Scalars['UUID']
pageLayoutId?: Scalars['UUID']
applicationId?: Scalars['UUID']
createdAt: Scalars['DateTime']
updatedAt: Scalars['DateTime']
__typename: 'CommandMenuItem'
}
export type EngineComponentKey = 'NAVIGATE_TO_NEXT_RECORD' | 'NAVIGATE_TO_PREVIOUS_RECORD' | 'CREATE_NEW_RECORD' | 'DELETE_RECORDS' | 'RESTORE_RECORDS' | 'DESTROY_RECORDS' | 'ADD_TO_FAVORITES' | 'REMOVE_FROM_FAVORITES' | 'EXPORT_NOTE_TO_PDF' | 'EXPORT_RECORDS' | 'UPDATE_MULTIPLE_RECORDS' | 'MERGE_MULTIPLE_RECORDS' | 'IMPORT_RECORDS' | 'EXPORT_VIEW' | 'SEE_DELETED_RECORDS' | 'CREATE_NEW_VIEW' | 'HIDE_DELETED_RECORDS' | 'EDIT_RECORD_PAGE_LAYOUT' | 'EDIT_DASHBOARD_LAYOUT' | 'SAVE_DASHBOARD_LAYOUT' | 'CANCEL_DASHBOARD_LAYOUT' | 'DUPLICATE_DASHBOARD' | 'ACTIVATE_WORKFLOW' | 'DEACTIVATE_WORKFLOW' | 'DISCARD_DRAFT_WORKFLOW' | 'TEST_WORKFLOW' | 'SEE_ACTIVE_VERSION_WORKFLOW' | 'SEE_RUNS_WORKFLOW' | 'SEE_VERSIONS_WORKFLOW' | 'ADD_NODE_WORKFLOW' | 'TIDY_UP_WORKFLOW' | 'DUPLICATE_WORKFLOW' | 'SEE_VERSION_WORKFLOW_RUN' | 'SEE_WORKFLOW_WORKFLOW_RUN' | 'STOP_WORKFLOW_RUN' | 'SEE_RUNS_WORKFLOW_VERSION' | 'SEE_WORKFLOW_WORKFLOW_VERSION' | 'USE_AS_DRAFT_WORKFLOW_VERSION' | 'SEE_VERSIONS_WORKFLOW_VERSION' | 'SEARCH_RECORDS' | 'SEARCH_RECORDS_FALLBACK' | 'ASK_AI' | 'VIEW_PREVIOUS_AI_CHATS' | 'NAVIGATION' | 'TRIGGER_WORKFLOW_VERSION' | 'FRONT_COMPONENT_RENDERER' | 'REPLY_TO_EMAIL_THREAD' | 'COMPOSE_EMAIL' | 'GO_TO_PEOPLE' | 'GO_TO_COMPANIES' | 'GO_TO_DASHBOARDS' | 'GO_TO_OPPORTUNITIES' | 'GO_TO_SETTINGS' | 'GO_TO_TASKS' | 'GO_TO_NOTES' | 'GO_TO_WORKFLOWS' | 'GO_TO_RUNS' | 'DELETE_SINGLE_RECORD' | 'DELETE_MULTIPLE_RECORDS' | 'RESTORE_SINGLE_RECORD' | 'RESTORE_MULTIPLE_RECORDS' | 'DESTROY_SINGLE_RECORD' | 'DESTROY_MULTIPLE_RECORDS' | 'EXPORT_FROM_RECORD_INDEX' | 'EXPORT_FROM_RECORD_SHOW' | 'EXPORT_MULTIPLE_RECORDS'
export type CommandMenuItemAvailabilityType = 'GLOBAL' | 'GLOBAL_OBJECT_CONTEXT' | 'RECORD_SELECTION' | 'FALLBACK'
export type CommandMenuItemPayload = (PathCommandMenuItemPayload | ObjectMetadataCommandMenuItemPayload) & { __isUnion?: true }
export interface PathCommandMenuItemPayload {
path: Scalars['String']
__typename: 'PathCommandMenuItemPayload'
}
export interface ObjectMetadataCommandMenuItemPayload {
objectMetadataItemId: Scalars['UUID']
__typename: 'ObjectMetadataCommandMenuItemPayload'
}
export interface ToolIndexEntry {
name: Scalars['String']
description: Scalars['String']
@@ -2241,7 +2238,7 @@ export interface ConnectedAccountDTO {
connectionParameters?: ImapSmtpCaldavConnectionParameters
lastSignedInAt?: Scalars['DateTime']
userWorkspaceId: Scalars['UUID']
connectionProviderId?: Scalars['UUID']
applicationConnectionProviderId?: Scalars['UUID']
applicationId?: Scalars['UUID']
name?: Scalars['String']
visibility: Scalars['String']
@@ -2275,7 +2272,7 @@ export interface ConnectedAccountPublicDTO {
scopes?: Scalars['String'][]
lastSignedInAt?: Scalars['DateTime']
userWorkspaceId: Scalars['UUID']
connectionProviderId?: Scalars['UUID']
applicationConnectionProviderId?: Scalars['UUID']
applicationId?: Scalars['UUID']
name?: Scalars['String']
visibility: Scalars['String']
@@ -2496,7 +2493,7 @@ export interface CollectionHash {
__typename: 'CollectionHash'
}
export type AllMetadataName = 'fieldMetadata' | 'objectMetadata' | 'view' | 'viewField' | 'viewFieldGroup' | 'viewGroup' | 'viewSort' | 'rowLevelPermissionPredicate' | 'rowLevelPermissionPredicateGroup' | 'viewFilterGroup' | 'index' | 'logicFunction' | 'viewFilter' | 'role' | 'roleTarget' | 'agent' | 'skill' | 'pageLayout' | 'pageLayoutWidget' | 'pageLayoutTab' | 'commandMenuItem' | 'navigationMenuItem' | 'permissionFlag' | 'objectPermission' | 'fieldPermission' | 'frontComponent' | 'webhook' | 'applicationVariable' | 'connectionProvider'
export type AllMetadataName = 'fieldMetadata' | 'objectMetadata' | 'view' | 'viewField' | 'viewFieldGroup' | 'viewGroup' | 'viewSort' | 'rowLevelPermissionPredicate' | 'rowLevelPermissionPredicateGroup' | 'viewFilterGroup' | 'index' | 'logicFunction' | 'viewFilter' | 'role' | 'roleTarget' | 'agent' | 'skill' | 'pageLayout' | 'pageLayoutWidget' | 'pageLayoutTab' | 'commandMenuItem' | 'navigationMenuItem' | 'permissionFlag' | 'objectPermission' | 'fieldPermission' | 'frontComponent' | 'webhook'
export interface MinimalObjectMetadata {
id: Scalars['UUID']
@@ -2927,7 +2924,6 @@ export interface ApplicationRegistrationGenqlSelection{
logoUrl?: boolean | number
createdAt?: boolean | number
updatedAt?: boolean | number
isConfigured?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
@@ -3149,49 +3145,6 @@ export interface FrontComponentGenqlSelection{
__scalar?: boolean | number
}
export interface CommandMenuItemGenqlSelection{
id?: boolean | number
workflowVersionId?: boolean | number
frontComponentId?: boolean | number
frontComponent?: FrontComponentGenqlSelection
engineComponentKey?: boolean | number
label?: boolean | number
icon?: boolean | number
shortLabel?: boolean | number
position?: boolean | number
isPinned?: boolean | number
availabilityType?: boolean | number
payload?: CommandMenuItemPayloadGenqlSelection
hotKeys?: boolean | number
conditionalAvailabilityExpression?: boolean | number
availabilityObjectMetadataId?: boolean | number
pageLayoutId?: boolean | number
universalIdentifier?: boolean | number
applicationId?: boolean | number
createdAt?: boolean | number
updatedAt?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface CommandMenuItemPayloadGenqlSelection{
on_PathCommandMenuItemPayload?:PathCommandMenuItemPayloadGenqlSelection,
on_ObjectMetadataCommandMenuItemPayload?:ObjectMetadataCommandMenuItemPayloadGenqlSelection,
__typename?: boolean | number
}
export interface PathCommandMenuItemPayloadGenqlSelection{
path?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface ObjectMetadataCommandMenuItemPayloadGenqlSelection{
objectMetadataItemId?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface LogicFunctionGenqlSelection{
id?: boolean | number
name?: boolean | number
@@ -3200,11 +3153,11 @@ export interface LogicFunctionGenqlSelection{
timeoutSeconds?: boolean | number
sourceHandlerPath?: boolean | number
handlerName?: boolean | number
toolInputSchema?: boolean | number
isTool?: boolean | number
cronTriggerSettings?: boolean | number
databaseEventTriggerSettings?: boolean | number
httpRouteTriggerSettings?: boolean | number
toolTriggerSettings?: boolean | number
workflowActionTriggerSettings?: boolean | number
applicationId?: boolean | number
universalIdentifier?: boolean | number
createdAt?: boolean | number
@@ -3380,7 +3333,6 @@ export interface ApplicationGenqlSelection{
defaultLogicFunctionRole?: RoleGenqlSelection
agents?: AgentGenqlSelection
frontComponents?: FrontComponentGenqlSelection
commandMenuItems?: CommandMenuItemGenqlSelection
logicFunctions?: LogicFunctionGenqlSelection
objects?: ObjectGenqlSelection
applicationVariables?: ApplicationVariableGenqlSelection
@@ -5116,6 +5068,48 @@ export interface PostgresCredentialsGenqlSelection{
__scalar?: boolean | number
}
export interface CommandMenuItemGenqlSelection{
id?: boolean | number
workflowVersionId?: boolean | number
frontComponentId?: boolean | number
frontComponent?: FrontComponentGenqlSelection
engineComponentKey?: boolean | number
label?: boolean | number
icon?: boolean | number
shortLabel?: boolean | number
position?: boolean | number
isPinned?: boolean | number
availabilityType?: boolean | number
payload?: CommandMenuItemPayloadGenqlSelection
hotKeys?: boolean | number
conditionalAvailabilityExpression?: boolean | number
availabilityObjectMetadataId?: boolean | number
pageLayoutId?: boolean | number
applicationId?: boolean | number
createdAt?: boolean | number
updatedAt?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface CommandMenuItemPayloadGenqlSelection{
on_PathCommandMenuItemPayload?:PathCommandMenuItemPayloadGenqlSelection,
on_ObjectMetadataCommandMenuItemPayload?:ObjectMetadataCommandMenuItemPayloadGenqlSelection,
__typename?: boolean | number
}
export interface PathCommandMenuItemPayloadGenqlSelection{
path?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface ObjectMetadataCommandMenuItemPayloadGenqlSelection{
objectMetadataItemId?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface ToolIndexEntryGenqlSelection{
name?: boolean | number
description?: boolean | number
@@ -5256,7 +5250,7 @@ export interface ConnectedAccountDTOGenqlSelection{
connectionParameters?: ImapSmtpCaldavConnectionParametersGenqlSelection
lastSignedInAt?: boolean | number
userWorkspaceId?: boolean | number
connectionProviderId?: boolean | number
applicationConnectionProviderId?: boolean | number
applicationId?: boolean | number
name?: boolean | number
visibility?: boolean | number
@@ -5293,7 +5287,7 @@ export interface ConnectedAccountPublicDTOGenqlSelection{
scopes?: boolean | number
lastSignedInAt?: boolean | number
userWorkspaceId?: boolean | number
connectionProviderId?: boolean | number
applicationConnectionProviderId?: boolean | number
applicationId?: boolean | number
name?: boolean | number
visibility?: boolean | number
@@ -6082,7 +6076,7 @@ export interface CreatePageLayoutWidgetInput {pageLayoutTabId: Scalars['UUID'],t
export interface UpdatePageLayoutWidgetInput {pageLayoutTabId?: (Scalars['UUID'] | null),title?: (Scalars['String'] | null),type?: (WidgetType | null),objectMetadataId?: (Scalars['UUID'] | null),gridPosition?: (GridPositionInput | null),position?: (Scalars['JSON'] | null),configuration?: (Scalars['JSON'] | null),conditionalDisplay?: (Scalars['JSON'] | null),conditionalAvailabilityExpression?: (Scalars['String'] | null)}
export interface CreateLogicFunctionFromSourceInput {id?: (Scalars['UUID'] | null),universalIdentifier?: (Scalars['UUID'] | null),name: Scalars['String'],description?: (Scalars['String'] | null),timeoutSeconds?: (Scalars['Float'] | null),source?: (Scalars['JSON'] | null),cronTriggerSettings?: (Scalars['JSON'] | null),databaseEventTriggerSettings?: (Scalars['JSON'] | null),httpRouteTriggerSettings?: (Scalars['JSON'] | null),toolTriggerSettings?: (Scalars['JSON'] | null),workflowActionTriggerSettings?: (Scalars['JSON'] | null)}
export interface CreateLogicFunctionFromSourceInput {id?: (Scalars['UUID'] | null),universalIdentifier?: (Scalars['UUID'] | null),name: Scalars['String'],description?: (Scalars['String'] | null),timeoutSeconds?: (Scalars['Float'] | null),toolInputSchema?: (Scalars['JSON'] | null),isTool?: (Scalars['Boolean'] | null),source?: (Scalars['JSON'] | null),cronTriggerSettings?: (Scalars['JSON'] | null),databaseEventTriggerSettings?: (Scalars['JSON'] | null),httpRouteTriggerSettings?: (Scalars['JSON'] | null)}
export interface ExecuteOneLogicFunctionInput {
/** Id of the logic function to execute */
@@ -6096,7 +6090,7 @@ id: Scalars['UUID'],
/** The logic function updates */
update: UpdateLogicFunctionFromSourceInputUpdates}
export interface UpdateLogicFunctionFromSourceInputUpdates {name?: (Scalars['String'] | null),description?: (Scalars['String'] | null),timeoutSeconds?: (Scalars['Float'] | null),sourceHandlerCode?: (Scalars['String'] | null),handlerName?: (Scalars['String'] | null),sourceHandlerPath?: (Scalars['String'] | null),cronTriggerSettings?: (Scalars['JSON'] | null),databaseEventTriggerSettings?: (Scalars['JSON'] | null),httpRouteTriggerSettings?: (Scalars['JSON'] | null),toolTriggerSettings?: (Scalars['JSON'] | null),workflowActionTriggerSettings?: (Scalars['JSON'] | null)}
export interface UpdateLogicFunctionFromSourceInputUpdates {name?: (Scalars['String'] | null),description?: (Scalars['String'] | null),timeoutSeconds?: (Scalars['Float'] | null),sourceHandlerCode?: (Scalars['String'] | null),toolInputSchema?: (Scalars['JSON'] | null),handlerName?: (Scalars['String'] | null),sourceHandlerPath?: (Scalars['String'] | null),isTool?: (Scalars['Boolean'] | null),cronTriggerSettings?: (Scalars['JSON'] | null),databaseEventTriggerSettings?: (Scalars['JSON'] | null),httpRouteTriggerSettings?: (Scalars['JSON'] | null)}
export interface CreateCommandMenuItemInput {workflowVersionId?: (Scalars['UUID'] | null),frontComponentId?: (Scalars['UUID'] | null),engineComponentKey: EngineComponentKey,label: Scalars['String'],icon?: (Scalars['String'] | null),shortLabel?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isPinned?: (Scalars['Boolean'] | null),availabilityType?: (CommandMenuItemAvailabilityType | null),hotKeys?: (Scalars['String'][] | null),conditionalAvailabilityExpression?: (Scalars['String'] | null),availabilityObjectMetadataId?: (Scalars['UUID'] | null),payload?: (Scalars['JSON'] | null),pageLayoutId?: (Scalars['UUID'] | null)}
@@ -6447,38 +6441,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const CommandMenuItem_possibleTypes: string[] = ['CommandMenuItem']
export const isCommandMenuItem = (obj?: { __typename?: any } | null): obj is CommandMenuItem => {
if (!obj?.__typename) throw new Error('__typename is missing in "isCommandMenuItem"')
return CommandMenuItem_possibleTypes.includes(obj.__typename)
}
const CommandMenuItemPayload_possibleTypes: string[] = ['PathCommandMenuItemPayload','ObjectMetadataCommandMenuItemPayload']
export const isCommandMenuItemPayload = (obj?: { __typename?: any } | null): obj is CommandMenuItemPayload => {
if (!obj?.__typename) throw new Error('__typename is missing in "isCommandMenuItemPayload"')
return CommandMenuItemPayload_possibleTypes.includes(obj.__typename)
}
const PathCommandMenuItemPayload_possibleTypes: string[] = ['PathCommandMenuItemPayload']
export const isPathCommandMenuItemPayload = (obj?: { __typename?: any } | null): obj is PathCommandMenuItemPayload => {
if (!obj?.__typename) throw new Error('__typename is missing in "isPathCommandMenuItemPayload"')
return PathCommandMenuItemPayload_possibleTypes.includes(obj.__typename)
}
const ObjectMetadataCommandMenuItemPayload_possibleTypes: string[] = ['ObjectMetadataCommandMenuItemPayload']
export const isObjectMetadataCommandMenuItemPayload = (obj?: { __typename?: any } | null): obj is ObjectMetadataCommandMenuItemPayload => {
if (!obj?.__typename) throw new Error('__typename is missing in "isObjectMetadataCommandMenuItemPayload"')
return ObjectMetadataCommandMenuItemPayload_possibleTypes.includes(obj.__typename)
}
const LogicFunction_possibleTypes: string[] = ['LogicFunction']
export const isLogicFunction = (obj?: { __typename?: any } | null): obj is LogicFunction => {
if (!obj?.__typename) throw new Error('__typename is missing in "isLogicFunction"')
@@ -7911,6 +7873,38 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const CommandMenuItem_possibleTypes: string[] = ['CommandMenuItem']
export const isCommandMenuItem = (obj?: { __typename?: any } | null): obj is CommandMenuItem => {
if (!obj?.__typename) throw new Error('__typename is missing in "isCommandMenuItem"')
return CommandMenuItem_possibleTypes.includes(obj.__typename)
}
const CommandMenuItemPayload_possibleTypes: string[] = ['PathCommandMenuItemPayload','ObjectMetadataCommandMenuItemPayload']
export const isCommandMenuItemPayload = (obj?: { __typename?: any } | null): obj is CommandMenuItemPayload => {
if (!obj?.__typename) throw new Error('__typename is missing in "isCommandMenuItemPayload"')
return CommandMenuItemPayload_possibleTypes.includes(obj.__typename)
}
const PathCommandMenuItemPayload_possibleTypes: string[] = ['PathCommandMenuItemPayload']
export const isPathCommandMenuItemPayload = (obj?: { __typename?: any } | null): obj is PathCommandMenuItemPayload => {
if (!obj?.__typename) throw new Error('__typename is missing in "isPathCommandMenuItemPayload"')
return PathCommandMenuItemPayload_possibleTypes.includes(obj.__typename)
}
const ObjectMetadataCommandMenuItemPayload_possibleTypes: string[] = ['ObjectMetadataCommandMenuItemPayload']
export const isObjectMetadataCommandMenuItemPayload = (obj?: { __typename?: any } | null): obj is ObjectMetadataCommandMenuItemPayload => {
if (!obj?.__typename) throw new Error('__typename is missing in "isObjectMetadataCommandMenuItemPayload"')
return ObjectMetadataCommandMenuItemPayload_possibleTypes.includes(obj.__typename)
}
const ToolIndexEntry_possibleTypes: string[] = ['ToolIndexEntry']
export const isToolIndexEntry = (obj?: { __typename?: any } | null): obj is ToolIndexEntry => {
if (!obj?.__typename) throw new Error('__typename is missing in "isToolIndexEntry"')
@@ -8310,82 +8304,6 @@ export const enumWorkspaceMemberNumberFormatEnum = {
APOSTROPHE_AND_DOT: 'APOSTROPHE_AND_DOT' as const
}
export const enumEngineComponentKey = {
NAVIGATE_TO_NEXT_RECORD: 'NAVIGATE_TO_NEXT_RECORD' as const,
NAVIGATE_TO_PREVIOUS_RECORD: 'NAVIGATE_TO_PREVIOUS_RECORD' as const,
CREATE_NEW_RECORD: 'CREATE_NEW_RECORD' as const,
DELETE_RECORDS: 'DELETE_RECORDS' as const,
RESTORE_RECORDS: 'RESTORE_RECORDS' as const,
DESTROY_RECORDS: 'DESTROY_RECORDS' as const,
ADD_TO_FAVORITES: 'ADD_TO_FAVORITES' as const,
REMOVE_FROM_FAVORITES: 'REMOVE_FROM_FAVORITES' as const,
EXPORT_NOTE_TO_PDF: 'EXPORT_NOTE_TO_PDF' as const,
EXPORT_RECORDS: 'EXPORT_RECORDS' as const,
UPDATE_MULTIPLE_RECORDS: 'UPDATE_MULTIPLE_RECORDS' as const,
MERGE_MULTIPLE_RECORDS: 'MERGE_MULTIPLE_RECORDS' as const,
IMPORT_RECORDS: 'IMPORT_RECORDS' as const,
EXPORT_VIEW: 'EXPORT_VIEW' as const,
SEE_DELETED_RECORDS: 'SEE_DELETED_RECORDS' as const,
CREATE_NEW_VIEW: 'CREATE_NEW_VIEW' as const,
HIDE_DELETED_RECORDS: 'HIDE_DELETED_RECORDS' as const,
EDIT_RECORD_PAGE_LAYOUT: 'EDIT_RECORD_PAGE_LAYOUT' as const,
EDIT_DASHBOARD_LAYOUT: 'EDIT_DASHBOARD_LAYOUT' as const,
SAVE_DASHBOARD_LAYOUT: 'SAVE_DASHBOARD_LAYOUT' as const,
CANCEL_DASHBOARD_LAYOUT: 'CANCEL_DASHBOARD_LAYOUT' as const,
DUPLICATE_DASHBOARD: 'DUPLICATE_DASHBOARD' as const,
ACTIVATE_WORKFLOW: 'ACTIVATE_WORKFLOW' as const,
DEACTIVATE_WORKFLOW: 'DEACTIVATE_WORKFLOW' as const,
DISCARD_DRAFT_WORKFLOW: 'DISCARD_DRAFT_WORKFLOW' as const,
TEST_WORKFLOW: 'TEST_WORKFLOW' as const,
SEE_ACTIVE_VERSION_WORKFLOW: 'SEE_ACTIVE_VERSION_WORKFLOW' as const,
SEE_RUNS_WORKFLOW: 'SEE_RUNS_WORKFLOW' as const,
SEE_VERSIONS_WORKFLOW: 'SEE_VERSIONS_WORKFLOW' as const,
ADD_NODE_WORKFLOW: 'ADD_NODE_WORKFLOW' as const,
TIDY_UP_WORKFLOW: 'TIDY_UP_WORKFLOW' as const,
DUPLICATE_WORKFLOW: 'DUPLICATE_WORKFLOW' as const,
SEE_VERSION_WORKFLOW_RUN: 'SEE_VERSION_WORKFLOW_RUN' as const,
SEE_WORKFLOW_WORKFLOW_RUN: 'SEE_WORKFLOW_WORKFLOW_RUN' as const,
STOP_WORKFLOW_RUN: 'STOP_WORKFLOW_RUN' as const,
SEE_RUNS_WORKFLOW_VERSION: 'SEE_RUNS_WORKFLOW_VERSION' as const,
SEE_WORKFLOW_WORKFLOW_VERSION: 'SEE_WORKFLOW_WORKFLOW_VERSION' as const,
USE_AS_DRAFT_WORKFLOW_VERSION: 'USE_AS_DRAFT_WORKFLOW_VERSION' as const,
SEE_VERSIONS_WORKFLOW_VERSION: 'SEE_VERSIONS_WORKFLOW_VERSION' as const,
SEARCH_RECORDS: 'SEARCH_RECORDS' as const,
SEARCH_RECORDS_FALLBACK: 'SEARCH_RECORDS_FALLBACK' as const,
ASK_AI: 'ASK_AI' as const,
VIEW_PREVIOUS_AI_CHATS: 'VIEW_PREVIOUS_AI_CHATS' as const,
NAVIGATION: 'NAVIGATION' as const,
TRIGGER_WORKFLOW_VERSION: 'TRIGGER_WORKFLOW_VERSION' as const,
FRONT_COMPONENT_RENDERER: 'FRONT_COMPONENT_RENDERER' as const,
REPLY_TO_EMAIL_THREAD: 'REPLY_TO_EMAIL_THREAD' as const,
COMPOSE_EMAIL: 'COMPOSE_EMAIL' as const,
GO_TO_PEOPLE: 'GO_TO_PEOPLE' as const,
GO_TO_COMPANIES: 'GO_TO_COMPANIES' as const,
GO_TO_DASHBOARDS: 'GO_TO_DASHBOARDS' as const,
GO_TO_OPPORTUNITIES: 'GO_TO_OPPORTUNITIES' as const,
GO_TO_SETTINGS: 'GO_TO_SETTINGS' as const,
GO_TO_TASKS: 'GO_TO_TASKS' as const,
GO_TO_NOTES: 'GO_TO_NOTES' as const,
GO_TO_WORKFLOWS: 'GO_TO_WORKFLOWS' as const,
GO_TO_RUNS: 'GO_TO_RUNS' as const,
DELETE_SINGLE_RECORD: 'DELETE_SINGLE_RECORD' as const,
DELETE_MULTIPLE_RECORDS: 'DELETE_MULTIPLE_RECORDS' as const,
RESTORE_SINGLE_RECORD: 'RESTORE_SINGLE_RECORD' as const,
RESTORE_MULTIPLE_RECORDS: 'RESTORE_MULTIPLE_RECORDS' as const,
DESTROY_SINGLE_RECORD: 'DESTROY_SINGLE_RECORD' as const,
DESTROY_MULTIPLE_RECORDS: 'DESTROY_MULTIPLE_RECORDS' as const,
EXPORT_FROM_RECORD_INDEX: 'EXPORT_FROM_RECORD_INDEX' as const,
EXPORT_FROM_RECORD_SHOW: 'EXPORT_FROM_RECORD_SHOW' as const,
EXPORT_MULTIPLE_RECORDS: 'EXPORT_MULTIPLE_RECORDS' as const
}
export const enumCommandMenuItemAvailabilityType = {
GLOBAL: 'GLOBAL' as const,
GLOBAL_OBJECT_CONTEXT: 'GLOBAL_OBJECT_CONTEXT' as const,
RECORD_SELECTION: 'RECORD_SELECTION' as const,
FALLBACK: 'FALLBACK' as const
}
export const enumFieldMetadataType = {
ACTOR: 'ACTOR' as const,
ADDRESS: 'ADDRESS' as const,
@@ -8745,6 +8663,82 @@ export const enumEmailingDomainStatus = {
TEMPORARY_FAILURE: 'TEMPORARY_FAILURE' as const
}
export const enumEngineComponentKey = {
NAVIGATE_TO_NEXT_RECORD: 'NAVIGATE_TO_NEXT_RECORD' as const,
NAVIGATE_TO_PREVIOUS_RECORD: 'NAVIGATE_TO_PREVIOUS_RECORD' as const,
CREATE_NEW_RECORD: 'CREATE_NEW_RECORD' as const,
DELETE_RECORDS: 'DELETE_RECORDS' as const,
RESTORE_RECORDS: 'RESTORE_RECORDS' as const,
DESTROY_RECORDS: 'DESTROY_RECORDS' as const,
ADD_TO_FAVORITES: 'ADD_TO_FAVORITES' as const,
REMOVE_FROM_FAVORITES: 'REMOVE_FROM_FAVORITES' as const,
EXPORT_NOTE_TO_PDF: 'EXPORT_NOTE_TO_PDF' as const,
EXPORT_RECORDS: 'EXPORT_RECORDS' as const,
UPDATE_MULTIPLE_RECORDS: 'UPDATE_MULTIPLE_RECORDS' as const,
MERGE_MULTIPLE_RECORDS: 'MERGE_MULTIPLE_RECORDS' as const,
IMPORT_RECORDS: 'IMPORT_RECORDS' as const,
EXPORT_VIEW: 'EXPORT_VIEW' as const,
SEE_DELETED_RECORDS: 'SEE_DELETED_RECORDS' as const,
CREATE_NEW_VIEW: 'CREATE_NEW_VIEW' as const,
HIDE_DELETED_RECORDS: 'HIDE_DELETED_RECORDS' as const,
EDIT_RECORD_PAGE_LAYOUT: 'EDIT_RECORD_PAGE_LAYOUT' as const,
EDIT_DASHBOARD_LAYOUT: 'EDIT_DASHBOARD_LAYOUT' as const,
SAVE_DASHBOARD_LAYOUT: 'SAVE_DASHBOARD_LAYOUT' as const,
CANCEL_DASHBOARD_LAYOUT: 'CANCEL_DASHBOARD_LAYOUT' as const,
DUPLICATE_DASHBOARD: 'DUPLICATE_DASHBOARD' as const,
ACTIVATE_WORKFLOW: 'ACTIVATE_WORKFLOW' as const,
DEACTIVATE_WORKFLOW: 'DEACTIVATE_WORKFLOW' as const,
DISCARD_DRAFT_WORKFLOW: 'DISCARD_DRAFT_WORKFLOW' as const,
TEST_WORKFLOW: 'TEST_WORKFLOW' as const,
SEE_ACTIVE_VERSION_WORKFLOW: 'SEE_ACTIVE_VERSION_WORKFLOW' as const,
SEE_RUNS_WORKFLOW: 'SEE_RUNS_WORKFLOW' as const,
SEE_VERSIONS_WORKFLOW: 'SEE_VERSIONS_WORKFLOW' as const,
ADD_NODE_WORKFLOW: 'ADD_NODE_WORKFLOW' as const,
TIDY_UP_WORKFLOW: 'TIDY_UP_WORKFLOW' as const,
DUPLICATE_WORKFLOW: 'DUPLICATE_WORKFLOW' as const,
SEE_VERSION_WORKFLOW_RUN: 'SEE_VERSION_WORKFLOW_RUN' as const,
SEE_WORKFLOW_WORKFLOW_RUN: 'SEE_WORKFLOW_WORKFLOW_RUN' as const,
STOP_WORKFLOW_RUN: 'STOP_WORKFLOW_RUN' as const,
SEE_RUNS_WORKFLOW_VERSION: 'SEE_RUNS_WORKFLOW_VERSION' as const,
SEE_WORKFLOW_WORKFLOW_VERSION: 'SEE_WORKFLOW_WORKFLOW_VERSION' as const,
USE_AS_DRAFT_WORKFLOW_VERSION: 'USE_AS_DRAFT_WORKFLOW_VERSION' as const,
SEE_VERSIONS_WORKFLOW_VERSION: 'SEE_VERSIONS_WORKFLOW_VERSION' as const,
SEARCH_RECORDS: 'SEARCH_RECORDS' as const,
SEARCH_RECORDS_FALLBACK: 'SEARCH_RECORDS_FALLBACK' as const,
ASK_AI: 'ASK_AI' as const,
VIEW_PREVIOUS_AI_CHATS: 'VIEW_PREVIOUS_AI_CHATS' as const,
NAVIGATION: 'NAVIGATION' as const,
TRIGGER_WORKFLOW_VERSION: 'TRIGGER_WORKFLOW_VERSION' as const,
FRONT_COMPONENT_RENDERER: 'FRONT_COMPONENT_RENDERER' as const,
REPLY_TO_EMAIL_THREAD: 'REPLY_TO_EMAIL_THREAD' as const,
COMPOSE_EMAIL: 'COMPOSE_EMAIL' as const,
GO_TO_PEOPLE: 'GO_TO_PEOPLE' as const,
GO_TO_COMPANIES: 'GO_TO_COMPANIES' as const,
GO_TO_DASHBOARDS: 'GO_TO_DASHBOARDS' as const,
GO_TO_OPPORTUNITIES: 'GO_TO_OPPORTUNITIES' as const,
GO_TO_SETTINGS: 'GO_TO_SETTINGS' as const,
GO_TO_TASKS: 'GO_TO_TASKS' as const,
GO_TO_NOTES: 'GO_TO_NOTES' as const,
GO_TO_WORKFLOWS: 'GO_TO_WORKFLOWS' as const,
GO_TO_RUNS: 'GO_TO_RUNS' as const,
DELETE_SINGLE_RECORD: 'DELETE_SINGLE_RECORD' as const,
DELETE_MULTIPLE_RECORDS: 'DELETE_MULTIPLE_RECORDS' as const,
RESTORE_SINGLE_RECORD: 'RESTORE_SINGLE_RECORD' as const,
RESTORE_MULTIPLE_RECORDS: 'RESTORE_MULTIPLE_RECORDS' as const,
DESTROY_SINGLE_RECORD: 'DESTROY_SINGLE_RECORD' as const,
DESTROY_MULTIPLE_RECORDS: 'DESTROY_MULTIPLE_RECORDS' as const,
EXPORT_FROM_RECORD_INDEX: 'EXPORT_FROM_RECORD_INDEX' as const,
EXPORT_FROM_RECORD_SHOW: 'EXPORT_FROM_RECORD_SHOW' as const,
EXPORT_MULTIPLE_RECORDS: 'EXPORT_MULTIPLE_RECORDS' as const
}
export const enumCommandMenuItemAvailabilityType = {
GLOBAL: 'GLOBAL' as const,
GLOBAL_OBJECT_CONTEXT: 'GLOBAL_OBJECT_CONTEXT' as const,
RECORD_SELECTION: 'RECORD_SELECTION' as const,
FALLBACK: 'FALLBACK' as const
}
export const enumCalendarChannelSyncStatus = {
NOT_SYNCED: 'NOT_SYNCED' as const,
ONGOING: 'ONGOING' as const,
@@ -8855,9 +8849,7 @@ export const enumAllMetadataName = {
objectPermission: 'objectPermission' as const,
fieldPermission: 'fieldPermission' as const,
frontComponent: 'frontComponent' as const,
webhook: 'webhook' as const,
applicationVariable: 'applicationVariable' as const,
connectionProvider: 'connectionProvider' as const
webhook: 'webhook' as const
}
export const enumEventLogTable = {
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -26,11 +26,11 @@ prod-run:
prod-postgres-run:
@docker run -d -p 5432:5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres --name twenty-postgres twenty-postgres:$(TAG)
prod-website-new-build:
@cd ../.. && docker build -f ./packages/twenty-docker/twenty-website-new/Dockerfile --platform $(PLATFORM) --tag twenty-website-new:$(TAG) . && cd -
prod-website-build:
@cd ../.. && docker build -f ./packages/twenty-docker/twenty-website/Dockerfile --platform $(PLATFORM) --tag twenty-website:$(TAG) . && cd -
prod-website-new-run:
@docker run -d -p 3000:3000 --name twenty-website-new twenty-website-new:$(TAG)
prod-website-run:
@docker run -d -p 3000:3000 --name twenty-website twenty-website:$(TAG)
# =============================================================================
# Local Development Services
@@ -34,27 +34,25 @@ has_schema=$(PGPASSWORD=twenty psql -h localhost -U twenty -d default -tAc \
"SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'core')")
if [ "$has_schema" = "f" ]; then
step_start "Running initial database setup and migrations"
yarn database:init:prod
step_start "Running initial database setup"
NODE_OPTIONS="--max-old-space-size=1500" node ./dist/database/scripts/setup-db.js
step_done
fi
step_start "Running migrations"
yarn database:migrate:prod --force
step_done
step_start "Flushing cache"
if ! yarn command:prod cache:flush; then
echo "Warning: Failed to flush cache before upgrade, but continuing startup..."
fi
yarn command:prod cache:flush
step_done
step_start "Running upgrade"
if ! yarn command:prod upgrade; then
echo "Warning: Upgrade completed with errors. Some workspaces may not be fully migrated. Check logs for details."
fi
yarn command:prod upgrade
step_done
step_start "Flushing cache"
if ! yarn command:prod cache:flush; then
echo "Warning: Failed to flush cache after upgrade, but continuing startup..."
fi
yarn command:prod cache:flush
step_done
# Only seed on first boot — check if the dev workspace already exists
@@ -0,0 +1,43 @@
FROM node:24-alpine AS twenty-website-build
WORKDIR /app
COPY ./package.json .
COPY ./yarn.lock .
COPY ./.yarnrc.yml .
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-ui/package.json /app/packages/twenty-ui/
COPY ./packages/twenty-shared/package.json /app/packages/twenty-shared/
COPY ./packages/twenty-website/package.json /app/packages/twenty-website/package.json
RUN yarn
ENV KEYSTATIC_GITHUB_CLIENT_ID="<fake build value>"
ENV KEYSTATIC_GITHUB_CLIENT_SECRET="<fake build value>"
ENV KEYSTATIC_SECRET="<fake build value>"
ENV NEXT_PUBLIC_KEYSTATIC_GITHUB_APP_SLUG="<fake build value>"
COPY ./packages/twenty-ui /app/packages/twenty-ui
COPY ./packages/twenty-website /app/packages/twenty-website
RUN npx nx build twenty-website
FROM node:24-alpine AS twenty-website
WORKDIR /app/packages/twenty-website
COPY --from=twenty-website-build /app /app
WORKDIR /app/packages/twenty-website
LABEL org.opencontainers.image.source=https://github.com/twentyhq/twenty
LABEL org.opencontainers.image.description="This image provides a consistent and reproducible environment for the website."
RUN chown -R 1000 /app
# Use non root user with uid 1000
USER 1000
CMD ["/bin/sh", "-c", "npx nx start"]
+3 -11
View File
@@ -16,17 +16,9 @@ setup_and_migrate_db() {
yarn database:init:prod
fi
if ! yarn command:prod cache:flush; then
echo "Warning: Failed to flush cache before upgrade, but continuing startup..."
fi
if ! yarn command:prod upgrade; then
echo "Warning: Upgrade completed with errors. Some workspaces may not be fully migrated. Check logs for details."
fi
if ! yarn command:prod cache:flush; then
echo "Warning: Failed to flush cache after upgrade, but continuing startup..."
fi
yarn command:prod cache:flush
yarn command:prod upgrade
yarn command:prod cache:flush
echo "Successfully migrated DB!"
}
+8 -3
View File
@@ -77,10 +77,14 @@ To deploy to Mintlify:
3. Set subdirectory to `packages/twenty-docs`
4. Mintlify will auto-deploy and generate search embeddings
## Status
## Next Steps
This migration has been completed. The legacy `packages/twenty-website` package
has been removed, and documentation now lives in `packages/twenty-docs`.
1. **Manual Review** - Check for any component conversion issues
2. **Fix Image Paths** - Verify all images render correctly
3. **Test Navigation** - Ensure all internal links work
4. **Deploy** - Push to production Mintlify
5. **Update Helper Agent** - Verify searchArticles tool works with full content
6. **Deprecate twenty-website docs** - Once migration is confirmed working
## Known Issues to Review
@@ -88,3 +92,4 @@ has been removed, and documentation now lives in `packages/twenty-docs`.
- Some images may have incorrect paths
- Custom styled components may need adjustment
- Video embeds might need review
@@ -15,7 +15,7 @@ Front components can render in two locations within Twenty:
## Basic example
The quickest way to see a front component in action is to register it with a **command menu item**. Use `defineCommandMenuItem` in a separate file to make the component appear as a quick-action button in the top-right corner of the page:
The quickest way to see a front component in action is to register it as a **command**. Adding a `command` field with `isPinned: true` makes it appear as a quick-action button in the top-right corner of the page — no page layout needed:
```tsx src/front-components/hello-world.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
@@ -34,20 +34,14 @@ export default defineFrontComponent({
name: 'hello-world',
description: 'A simple front component',
component: HelloWorld,
});
```
```ts src/command-menu-items/hello-world.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
shortLabel: 'Hello',
label: 'Hello World',
icon: 'IconBolt',
isPinned: true,
availabilityType: 'GLOBAL',
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
command: {
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
shortLabel: 'Hello',
label: 'Hello World',
icon: 'IconBolt',
isPinned: true,
availabilityType: 'GLOBAL',
},
});
```
@@ -68,6 +62,7 @@ Click it to render the component inline.
| `name` | No | Display name |
| `description` | No | Description of what the component does |
| `isHeadless` | No | Set to `true` if the component has no visible UI (see below) |
| `command` | No | Register the component as a command (see [command options](#command-options) below) |
## Placing a front component on a page
@@ -146,17 +141,11 @@ export default defineFrontComponent({
description: 'Creates a task from the command menu',
component: RunAction,
isHeadless: true,
});
```
```ts src/command-menu-items/run-action.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
icon: 'IconPlayerPlay',
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
command: {
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
icon: 'IconPlayerPlay',
},
});
```
@@ -188,6 +177,11 @@ export default defineFrontComponent({
description: 'Deletes a draft with confirmation',
component: DeleteDraft,
isHeadless: true,
command: {
universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
label: 'Delete draft',
icon: 'IconTrash',
},
});
```
@@ -229,8 +223,7 @@ Available hooks:
| Hook | Returns | Description |
|------|---------|-------------|
| `useUserId()` | `string` or `null` | The current user's ID |
| `useSelectedRecordIds()` | `string[]` | All selected record IDs (empty array if none selected) |
| `useRecordId()` | `string` or `null` | **Deprecated.** Use `useSelectedRecordIds()` instead |
| `useRecordId()` | `string` or `null` | The current record's ID (when placed on a record page) |
| `useFrontComponentId()` | `string` | This component instance's ID |
| `useFrontComponentExecutionContext(selector)` | varies | Access the full execution context with a selector function |
@@ -293,84 +286,14 @@ export default defineFrontComponent({
});
```
### Working with multiple records
## Command options
Use `useSelectedRecordIds()` to handle multiple selected records. This is useful for bulk operations:
```tsx src/front-components/bulk-export.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds, numberOfSelectedRecords } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-sdk/clients';
const BulkExport = () => {
const selectedRecordIds = useSelectedRecordIds();
const handleExport = async () => {
const client = new CoreApiClient();
for (const recordId of selectedRecordIds) {
await client.mutation({
updateTask: {
__args: { id: recordId, data: { exported: true } },
id: true,
},
});
}
await enqueueSnackbar({
message: `Exported ${selectedRecordIds.length} records`,
variant: 'success',
});
await closeSidePanel();
};
return (
<div style={{ padding: '20px' }}>
<p>Export {selectedRecordIds.length} selected record(s)?</p>
<button onClick={handleExport}>Export</button>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
name: 'bulk-export',
description: 'Export selected records',
component: BulkExport,
command: {
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902',
label: 'Bulk Export',
availabilityType: 'RECORD_SELECTION',
conditionalAvailabilityExpression: numberOfSelectedRecords > 0,
},
});
```
## defineCommandMenuItem
Use `defineCommandMenuItem` to register a front component in the command menu (Cmd+K). If `isPinned` is `true`, it also appears as a quick-action button in the top-right corner of the page.
```ts src/command-menu-items/open-dashboard.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
label: 'Open Dashboard',
shortLabel: 'Dashboard',
icon: 'IconLayoutDashboard',
isPinned: true,
availabilityType: 'GLOBAL',
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
});
```
Adding a `command` field to `defineFrontComponent` registers the component in the command menu (Cmd+K). If `isPinned` is `true`, it also appears as a quick-action button in the top-right corner of the page.
| Field | Required | Description |
|-------|----------|-------------|
| `universalIdentifier` | Yes | Stable unique ID for the command |
| `label` | Yes | Full label shown in the command menu (Cmd+K) |
| `frontComponentUniversalIdentifier` | Yes | The `universalIdentifier` of the front component this command opens |
| `shortLabel` | No | Shorter label displayed on the pinned quick-action button |
| `icon` | No | Icon name displayed next to the label (e.g. `'IconBolt'`, `'IconSend'`) |
| `isPinned` | No | When `true`, shows the command as a quick-action button in the top-right corner of the page |
@@ -382,23 +305,30 @@ export default defineCommandMenuItem({
The `conditionalAvailabilityExpression` field lets you control when a command is visible based on the current page context. Import typed variables and operators from `twenty-sdk` to build expressions:
```ts src/command-menu-items/bulk-update.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
```tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import {
pageType,
numberOfSelectedRecords,
objectPermissions,
everyEquals,
isDefined,
} from 'twenty-sdk/front-component';
export default defineCommandMenuItem({
export default defineFrontComponent({
universalIdentifier: '...',
label: 'Bulk Update',
availabilityType: 'RECORD_SELECTION',
frontComponentUniversalIdentifier: '...',
conditionalAvailabilityExpression: everyEquals(
objectPermissions,
'canUpdateObjectRecords',
true,
),
name: 'bulk-action',
component: BulkAction,
command: {
universalIdentifier: '...',
label: 'Bulk Update',
availabilityType: 'RECORD_SELECTION',
conditionalAvailabilityExpression: everyEquals(
objectPermissions,
'canUpdateObjectRecords',
true,
),
},
});
```
@@ -4,52 +4,48 @@ icon: "rocket"
description: Create your first Twenty app in minutes.
---
## What are apps?
Apps let you extend Twenty with custom objects, fields, logic functions, front components, AI skills, and more — all managed as code. Instead of configuring everything through the UI, you define your data model and logic in TypeScript and deploy it to one or more workspaces.
## Prerequisites
- **Node.js 24+** — [Download](https://nodejs.org/)
- **Yarn 4** — bundled with Node via Corepack. Enable it: `corepack enable`
- **Docker** — [Download](https://www.docker.com/products/docker-desktop/). Needed to run a local Twenty server. Skip if you already have Twenty running elsewhere.
Before you begin, make sure the following is installed on your machine:
Building a Twenty app has three phases. The scaffolder collapses them into one happy-path command, but each phase is a separate concept — when something fails, knowing which phase you're in tells you what to fix.
- **Node.js 24+** — [Download here](https://nodejs.org/)
- **Yarn 4** — Comes with Node.js via Corepack. Enable it by running `corepack enable`
- **Docker** — [Download here](https://www.docker.com/products/docker-desktop/). Required to run a local Twenty instance. Not needed if you already have a Twenty server running.
| Phase | What you do | Tool | Result |
|---|---|---|---|
| **1. Scaffold** | Generate the app's source code | `npx create-twenty-app` | A TypeScript project on disk |
| **2. Run a server** | Start a Twenty server to sync into | Docker + `yarn twenty server` | A running Twenty instance |
| **3. Sync** | Live-sync your code to the server | `yarn twenty dev` | Your changes appear in the UI |
## Create your first app
---
### Scaffold your app
## Phase 1 — Scaffold your project
Create a new app from the template:
Open a terminal and run:
```bash filename="Terminal"
npx create-twenty-app@latest my-twenty-app
```
You'll be prompted for a name and description — press **Enter** for the defaults. This generates a TypeScript project in `my-twenty-app/` with a starter `application-config.ts`, a default role, a CI workflow, and an integration test.
You will be prompted to enter a name and a description for your app. Press **Enter** to accept the defaults.
**After this phase:** you have an app's source code on your machine. It isn't running yet — that's Phase 2.
This creates a new folder called `my-twenty-app` with everything you need.
---
### Set up a local Twenty instance
## Phase 2 — Run a local Twenty server
Your app needs a Twenty server to sync into. The server is a full Twenty instance — UI, GraphQL API, PostgreSQL — running locally in Docker. Your local code uploads its definitions to that server, which makes them appear in the UI.
The scaffolder offers to start one for you:
The scaffolder will ask:
> **Would you like to set up a local Twenty instance?**
- **Yes (recommended)** — pulls the `twentycrm/twenty-app-dev` Docker image and starts it on port `2020`. Make sure Docker is running first.
- **No** — choose this if you already have a Twenty server you want to connect to. You can wire it up later with `yarn twenty remote add`.
- **Type `yes`** (recommended) — This pulls the `twenty-app-dev` Docker image and starts a local Twenty server on port `2020`. Make sure Docker is running before you continue.
- **Type `no`** — Choose this if you already have a Twenty server running locally.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/start-instance.png" alt="Should start local instance?" />
</div>
Once the server is up, a browser opens for sign-in. Use the pre-seeded demo account:
### Sign in to your workspace
Next, a browser window will open with the Twenty login page. Sign in with the pre-seeded demo account:
- **Email:** `tim@apple.dev`
- **Password:** `tim@apple.dev`
@@ -58,66 +54,50 @@ Once the server is up, a browser opens for sign-in. Use the pre-seeded demo acco
<img src="/images/docs/developers/extends/apps/login.png" alt="Twenty login screen" />
</div>
Click **Authorize** on the next screen — this gives the CLI access to your workspace.
### Authorize the app
After you sign in, you will see an authorization screen. This lets your app interact with your workspace.
Click **Authorize** to continue.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/authorize.png" alt="Twenty CLI authorization screen" />
</div>
Your terminal will confirm everything is set up.
Once authorized, your terminal will confirm that everything is set up.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/scaffolded.png" alt="App scaffolded successfully" />
</div>
**After this phase:** you have a running Twenty server at [http://localhost:2020](http://localhost:2020) with your CLI authorized to sync to it.
### Start developing
<Note>
If Docker isn't installed or running, the scaffolder will tell you the right start command for your OS. Once Docker is up, you can resume with `yarn twenty server start` — no need to re-scaffold.
</Note>
---
## Phase 3 — Sync your changes
This is the inner loop you'll spend most of your time in.
Go into your new app folder and start the development server:
```bash filename="Terminal"
cd my-twenty-app
yarn twenty dev
```
This watches `src/`, rebuilds on every change, and syncs the result to the server. Edit a file, save, and within a second the server reflects the change. You'll see a live status panel in your terminal.
This watches your source files, rebuilds on every change, and syncs your app to the local Twenty server automatically. You should see a live status panel in your terminal.
For more detailed output (build logs, sync requests, error traces), add `--verbose`.
For more detailed output (build logs, sync requests, error traces), use the `--verbose` flag:
```bash filename="Terminal"
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` 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.png" alt="Dev mode terminal output" />
</div>
Open [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer). You should see your app under **Your Apps**.
#### One-shot sync with `yarn twenty dev --once`
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-1.png" alt="Your Apps list showing My twenty app" />
</div>
Click **My twenty app** to see its **application registration** — a server-level record describing your app (name, identifier, OAuth credentials, source). One registration can be installed across multiple workspaces on the same server.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-2.png" alt="Application registration details" />
</div>
Click **View installed app** to see the workspace install. The **About** tab shows version and management options.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Installed app" />
</div>
**After this phase:** you have a live development loop. Edit any file in `src/` and it appears in the UI.
### One-shot sync for CI and scripts
Pass `--once` to run a single build + sync and exit — same pipeline, no watcher:
If you do not want a watcher running in the background (for example in a CI pipeline, a git hook, or a scripted workflow), pass the `--once` flag. It runs the same pipeline as `yarn twenty dev` — build manifest, bundle files, upload, sync, regenerate the typed API client — but **exits as soon as the sync completes**:
```bash filename="Terminal"
yarn twenty dev --once
@@ -125,14 +105,32 @@ yarn twenty dev --once
| Command | Behavior | When to use |
|---------|----------|-------------|
| `yarn twenty dev` | Watches and re-syncs on every change. Runs until you stop it. | Interactive local development. |
| `yarn twenty dev --once` | Single build + sync, exits `0` on success, `1` on failure. | CI, pre-commit hooks, AI agents, scripted workflows. |
| `yarn twenty dev` | Watches your source files and re-syncs on every change. Keeps running until you stop it. | Interactive local development — you want the live status panel and instant feedback loop. |
| `yarn twenty dev --once` | Performs a single build + sync, then exits with code `0` on success or `1` on failure. | Scripts, CI, pre-commit hooks, AI agents, and any non-interactive workflow. |
Both modes need a server in development mode and an authenticated remote.
Both modes require a Twenty server running in development mode and an authenticated remote — the same prerequisites apply.
<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).
</Warning>
### See your app in Twenty
Open [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer) in your browser. Navigate to **Settings > Apps** and select the **Developer** tab. You should see your app listed under **Your Apps**:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-1.png" alt="Your Apps list showing My twenty app" />
</div>
Click on **My twenty app** to open its **application registration**. A registration is a server-level record that describes your app — its name, unique identifier, OAuth credentials, and source (local, npm, or tarball). It lives on the server, not inside any specific workspace. When you install an app into a workspace, Twenty creates a workspace-scoped **application** that points back to this registration. One registration can be installed across multiple workspaces on the same server.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-2.png" alt="Application registration details" />
</div>
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" />
</div>
You are all set! Edit any file in `src/` and the changes will be picked up automatically.
---
@@ -142,110 +140,134 @@ Apps are composed of **entities** — each defined as a TypeScript file with a s
| Entity | What it does |
|--------|-------------|
| **Objects & Fields** | Custom data models (Post Card, Invoice, etc.) with typed fields |
| **Logic functions** | Server-side TypeScript triggered by HTTP routes, cron schedules, or database events |
| **Objects & Fields** | Define custom data models (like Post Card, Invoice) with typed fields |
| **Logic functions** | Server-side TypeScript functions triggered by HTTP routes, cron schedules, or database events |
| **Front components** | React components that render inside Twenty's UI (side panel, widgets, command menu) |
| **Skills & Agents** | AI capabilities — reusable instructions and autonomous assistants |
| **Views & Navigation** | Pre-configured list views and sidebar menu items |
| **Views & Navigation** | Pre-configured list views and sidebar menu items for your objects |
| **Page layouts** | Custom record detail pages with tabs and widgets |
Full reference: [Building Apps](/developers/extend/apps/building).
Head over to [Building Apps](/developers/extend/apps/building) for a detailed guide on each entity type.
---
## Project structure
The scaffolder generates the following file structure:
```text filename="my-twenty-app/"
my-twenty-app/
package.json
yarn.lock
.gitignore
.nvmrc
.yarnrc.yml
.oxlintrc.json
tsconfig.json
tsconfig.spec.json # TypeScript config for tests
vitest.config.ts # Vitest test runner configuration
LLMS.md
README.md
.github/
└── workflows/
└── ci.yml # GitHub Actions CI workflow
public/ # Public assets (images, fonts, etc.)
src/
application-config.ts # Required — your app's entry point
default-role.ts # Permissions for logic functions
constants/
universal-identifiers.ts # Auto-generated UUIDs and metadata
__tests__/
setup-test.ts
app-install.integration-test.ts
.github/workflows/ci.yml # GitHub Actions
public/ # Static assets
vitest.config.ts # Test runner config
tsconfig.json, tsconfig.spec.json
.nvmrc, .yarnrc.yml, .oxlintrc.json
README.md, LLMS.md
├── application-config.ts # Required — main application configuration
├── default-role.ts # Default role for logic functions
├── constants/
└── universal-identifiers.ts # Auto-generated UUIDs and app metadata
└── __tests__/
├── setup-test.ts # Test setup (server health check, config)
└── app-install.integration-test.ts # Integration test
```
| File / Folder | Purpose |
|---|---|
| `src/application-config.ts` | **Required.** The main configuration file for your app. |
| `src/default-role.ts` | Default role controlling what your logic functions can access. |
| `src/constants/universal-identifiers.ts` | Auto-generated UUIDs and metadata (display name, description). |
| `src/__tests__/` | Integration tests (setup + example test). |
| `public/` | Static assets (images, fonts) served with your app. |
### Starting from an example
Use `--example` to start with a more complete project (custom objects, fields, logic functions, front components):
To start from a more complete example with custom objects, fields, logic functions, front components, and more, use the `--example` flag:
```bash filename="Terminal"
npx create-twenty-app@latest my-twenty-app --example postcard
```
Examples live in [twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples). You can also scaffold individual entities into an existing project with `yarn twenty add` see [Building Apps](/developers/extend/apps/building#scaffolding-entities-with-yarn-twenty-add).
Examples are sourced from the [twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples) directory on GitHub. You can also scaffold individual entities into an existing project with `yarn twenty add` (see [Building Apps](/developers/extend/apps/building#scaffolding-entities-with-yarn-twenty-add)).
---
### Key files
## Managing the local server
| File / Folder | Purpose |
|---|---|
| `package.json` | Declares your app name, version, and dependencies. Includes a `twenty` script so you can run `yarn twenty help` to see all commands. |
| `src/application-config.ts` | **Required.** The main configuration file for your app. |
| `src/default-role.ts` | Default role that controls what your logic functions can access. |
| `src/constants/universal-identifiers.ts` | Auto-generated UUIDs and app metadata (display name, description). |
| `src/__tests__/` | Integration tests (setup + example test). |
| `public/` | Static assets (images, fonts) served with your app. |
Use `yarn twenty server` to control the local Twenty container:
## Local development server
| Command | What it does |
|---------|--------------|
| `yarn twenty server start` | Start the server (pulls the image if needed) |
The scaffolder already started a local Twenty server for you. To manage it later, use `yarn twenty server`:
| Command | Description |
|---------|-------------|
| `yarn twenty server start` | Start the local server (pulls image if needed) |
| `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 URL, version, and login credentials |
| `yarn twenty server status` | Show server status, URL, version, and credentials |
| `yarn twenty server logs` | Stream server logs |
| `yarn twenty server reset` | Wipe data and start fresh |
| `yarn twenty server upgrade` | Pull the latest `twenty-app-dev` image |
| `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 persists across restarts in two Docker volumes (`twenty-app-dev-data` for PostgreSQL, `twenty-app-dev-storage` for files). Use `reset` to wipe everything.
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
`yarn twenty server upgrade` pulls the latest image, compares digests, and only recreates the container if anything actually changed. Volumes are preserved — only the container is replaced. If a new image was pulled and the container was running, the upgrade automatically starts a new container; run `yarn twenty server start` afterward to wait for it to become healthy.
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"
yarn twenty server upgrade # Latest
yarn twenty server upgrade 2.2.0 # Specific version
# 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
```
Verify the running version with `yarn twenty server status` (it shows the `APP_VERSION` baked into the container).
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.
### Running a parallel test instance
You can verify the running version with `yarn twenty server status`, which displays the `APP_VERSION` from the container.
Pass `--test` to any `server` command to manage a second, fully isolated instance — useful for integration tests or experiments without touching your main dev data:
### Running a test instance
| Command | What it does |
|---------|--------------|
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.
| Command | Description |
|---------|-------------|
| `yarn twenty server start --test` | Start the test instance (defaults to port 2021) |
| `yarn twenty server stop --test` | Stop it |
| `yarn twenty server status --test` | Show its status |
| `yarn twenty server logs --test` | Stream its logs |
| `yarn twenty server reset --test` | Wipe its data |
| `yarn twenty server upgrade --test` | Upgrade its image |
| `yarn twenty server stop --test` | Stop the test instance |
| `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 has its own container (`twenty-app-dev-test`), volumes (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`), and config — it runs alongside your main instance without conflicts. Combine `--test` with `--port` to override 2021.
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.
---
<Note>
The server requires **Docker** to be running. If you see a "Docker not running" error, make sure Docker Desktop (or the Docker daemon) is started.
</Note>
## Manual setup (without the scaffolder)
Skip the scaffolder if you're adding the SDK to an existing project:
If you prefer to set things up yourself instead of using `create-twenty-app`, you can do it in two steps.
**1. Add `twenty-sdk` and `twenty-client-sdk` as dependencies:**
```bash filename="Terminal"
yarn add twenty-sdk twenty-client-sdk
```
Add the script to `package.json`:
**2. Add a `twenty` script to your `package.json`:**
```json filename="package.json"
{
@@ -255,19 +277,19 @@ Add the script to `package.json`:
}
```
You can now run `yarn twenty dev`, `yarn twenty server start`, and the rest.
You can now run `yarn twenty dev`, `yarn twenty help`, and all other commands.
<Note>
Don't install `twenty-sdk` globally — pin it per project so each app uses its own version.
Do not install `twenty-sdk` globally. Always use it as a local project dependency so that each project can pin its own version.
</Note>
---
## Troubleshooting
- **Docker errors** — Make sure Docker Desktop (or the daemon) is running before `yarn twenty server start`. The error message will show the right start command for your OS.
- **Wrong Node version** — Need 24+. Check with `node -v`.
- **Yarn 4 missing** — Run `corepack enable`.
- **Dependencies broken** — `rm -rf node_modules && yarn install`.
If you run into issues:
Stuck? Ask on the [Twenty Discord](https://discord.com/channels/1130383047699738754/1130386664812982322).
- Make sure **Docker is running** before starting the scaffolder with a local instance.
- Make sure you are using **Node.js 24+** (`node -v` to check).
- Make sure **Corepack is enabled** (`corepack enable`) so Yarn 4 is available.
- Try deleting `node_modules` and running `yarn install` again if dependencies seem broken.
Still stuck? Ask for help on the [Twenty Discord](https://discord.com/channels/1130383047699738754/1130386664812982322).
@@ -76,28 +76,6 @@ yarn twenty logs
```
</Note>
#### enqueueLogicFunctionExecution
Inside your logic function handler — regardless of trigger (HTTP route, cron, database event, tool, workflow action, or install hook) — you can enqueue **another** function that belongs to the **same application** on the worker queue. Import it from `twenty-sdk/logic-function`:
```ts
import {
enqueueLogicFunctionExecution,
type RoutePayload,
} from 'twenty-sdk/logic-function';
const handler = async (event: RoutePayload) => {
const { jobId, status } = await enqueueLogicFunctionExecution({
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
payload: { fromWebhook: true },
});
return { accepted: true, jobId, status };
};
```
Pass **exactly one** of `name` or `universalIdentifier`.
#### Route trigger payload
When a route trigger invokes your logic function, it receives a `RoutePayload` object that follows the
@@ -164,14 +142,11 @@ const handler = async (event: RoutePayload) => {
Header names are normalized to lowercase. Access them using lowercase keys (e.g., `event.headers['content-type']`).
</Note>
#### Exposing a function as an AI tool or workflow action
#### Exposing a function as a tool
Logic functions can be exposed on two surfaces, each with its own trigger:
Logic functions can be exposed as **tools** for AI agents and workflows. When marked as a tool, a function becomes discoverable by Twenty's AI features and can be used in workflow automations.
- **`toolTriggerSettings`** — makes the function discoverable by Twenty's AI features (chat, MCP, function calling). Uses standard JSON Schema, the format LLMs natively understand.
- **`workflowActionTriggerSettings`** — makes the function appear as a step in the visual workflow builder. Uses Twenty's rich `InputSchema` so the builder can render proper field editors, variable pickers, and labels.
A function can opt into one, the other, or both. They sit alongside `cronTriggerSettings`, `databaseEventTriggerSettings`, and `httpRouteTriggerSettings` — same pattern, same shape.
To mark a logic function as a tool, set `isTool: true`:
```ts src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
@@ -201,33 +176,31 @@ export default defineLogicFunction({
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
toolTriggerSettings: {},
isTool: true,
});
```
Key points:
- A function can mix surfaces — declare both `toolTriggerSettings` and `workflowActionTriggerSettings` to expose it in chat AND in the workflow builder.
- `toolTriggerSettings.inputSchema` and `workflowActionTriggerSettings.inputSchema` are both optional. When omitted, the manifest builder infers them from the handler source code (JSON Schema for the AI tool, Twenty's `InputSchema` for the workflow action). Provide one explicitly when you want richer typing — for example, with `FieldMetadataType`-aware fields like `CURRENCY` or `RELATION` for the workflow builder, or with `description` fields the AI agent can read:
- You can combine `isTool` with triggers — a function can be both a tool (callable by AI agents) and triggered by events at the same time.
- **`toolInputSchema`** (optional): A JSON Schema object describing the parameters your function accepts. The schema is computed automatically from source code static analysis, but you can set it explicitly:
```ts
export default defineLogicFunction({
...,
toolTriggerSettings: {
inputSchema: {
type: 'object',
properties: {
companyName: {
type: 'string',
description: 'The name of the company to enrich',
},
domain: {
type: 'string',
description: 'The company website domain (optional)',
},
toolInputSchema: {
type: 'object',
properties: {
companyName: {
type: 'string',
description: 'The name of the company to enrich',
},
domain: {
type: 'string',
description: 'The company website domain (optional)',
},
required: ['companyName'],
},
required: ['companyName'],
},
});
```
@@ -266,7 +239,7 @@ yarn twenty exec --postInstall
```
Key points:
- Post-install functions use `definePostInstallLogicFunction()` — a specialized variant that omits trigger settings (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `toolTriggerSettings`, `workflowActionTriggerSettings`).
- Post-install functions use `definePostInstallLogicFunction()` — a specialized variant that omits trigger settings (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
- The handler receives an `InstallPayload` with `{ previousVersion?: string; newVersion: string }` — `newVersion` is the version being installed, and `previousVersion` is the version that was previously installed (or `undefined` on a fresh install). Use these values to distinguish fresh installs from upgrades and to run version-specific migration logic.
- **When the hook runs**: on fresh installs only, by default. Pass `shouldRunOnVersionUpgrade: true` if you also want it to run when the app is upgraded from a previous version. When omitted, the flag defaults to `false` and upgrades skip the hook.
- **Execution model — async by default, sync opt-in**: the `shouldRunSynchronously` flag controls *how* post-install is executed.
@@ -198,14 +198,6 @@ export default defineApplication({
See the [defineApplication accordion](/developers/extend/apps/building#defineentity-functions) in the Building Apps page for the full list of marketplace fields (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl`, etc.).
#### Recommended screenshot dimensions
The marketplace renders `screenshots` in a fixed `8:5` container (for example, `1600×1000 px`).
<Note>
Screenshots of any aspect ratio are displayed in full and are never cropped, but anything significantly taller or narrower than `8:5` will show empty bands on the sides.
</Note>
### Publish
```bash filename="Terminal"
@@ -225,9 +217,9 @@ The Twenty server syncs its marketplace catalog from the npm registry **every ho
You can trigger the sync immediately instead of waiting:
```bash filename="Terminal"
yarn twenty server catalog-sync
yarn twenty catalog-sync
# To target a specific remote:
# yarn twenty server catalog-sync --remote production
# yarn twenty catalog-sync --remote production
```
The metadata shown in the marketplace comes from your `defineApplication()` config — fields like `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl`, and `termsUrl`.
+18 -18
View File
@@ -3866,10 +3866,10 @@
"language": "ro",
"tabs": [
{
"tab": "Începeți",
"tab": "Getting Started",
"groups": [
{
"group": "Bun venit",
"group": "Welcome",
"pages": [
"l/ro/getting-started/introduction",
"l/ro/getting-started/key-features",
@@ -3877,7 +3877,7 @@
]
},
{
"group": "Concepte cheie",
"group": "Core Concepts",
"pages": [
"l/ro/getting-started/core-concepts/data-model",
"l/ro/getting-started/core-concepts/layout",
@@ -3895,7 +3895,7 @@
"tab": "User Guide",
"groups": [
{
"group": "Prezentare generală",
"group": "Overview",
"pages": [
"l/ro/user-guide/introduction"
]
@@ -3906,7 +3906,7 @@
"pages": [
"l/ro/user-guide/data-model/overview",
{
"group": "Referință",
"group": "Reference",
"pages": [
"l/ro/user-guide/data-model/capabilities/objects",
"l/ro/user-guide/data-model/capabilities/fields",
@@ -3932,7 +3932,7 @@
"pages": [
"l/ro/user-guide/data-migration/overview",
{
"group": "Referință",
"group": "Reference",
"pages": [
"l/ro/user-guide/data-migration/capabilities/file-formats",
"l/ro/user-guide/data-migration/capabilities/field-mapping",
@@ -3964,7 +3964,7 @@
"pages": [
"l/ro/user-guide/calendar-emails/overview",
{
"group": "Referință",
"group": "Reference",
"pages": [
"l/ro/user-guide/calendar-emails/capabilities/mailbox",
"l/ro/user-guide/calendar-emails/capabilities/calendar"
@@ -3989,7 +3989,7 @@
"pages": [
"l/ro/user-guide/workflows/overview",
{
"group": "Referință",
"group": "Reference",
"pages": [
"l/ro/user-guide/workflows/capabilities/workflow-triggers",
"l/ro/user-guide/workflows/capabilities/workflow-actions",
@@ -4052,7 +4052,7 @@
"pages": [
"l/ro/user-guide/ai/overview",
{
"group": "Referință",
"group": "Reference",
"pages": [
"l/ro/user-guide/ai/capabilities/ai-chatbot",
"l/ro/user-guide/ai/capabilities/ai-agents",
@@ -4068,16 +4068,16 @@
]
},
{
"group": "Aspect",
"group": "Layout",
"icon": "table-columns",
"pages": [
"l/ro/user-guide/layout/overview",
{
"group": "Referință",
"group": "Reference",
"pages": [
"l/ro/user-guide/layout/capabilities/navigation",
{
"group": "Vizualizări",
"group": "Views",
"pages": [
"l/ro/user-guide/views-pipelines/capabilities/table-views",
"l/ro/user-guide/views-pipelines/capabilities/kanban-views",
@@ -4091,7 +4091,7 @@
]
},
{
"group": "Ghiduri practice",
"group": "How-Tos",
"pages": [
"l/ro/user-guide/views-pipelines/how-tos/create-a-table-view-with-grouping",
"l/ro/user-guide/views-pipelines/how-tos/create-a-kanban-view-for-projects",
@@ -4110,7 +4110,7 @@
"pages": [
"l/ro/user-guide/dashboards/overview",
{
"group": "Referință",
"group": "Reference",
"pages": [
"l/ro/user-guide/dashboards/capabilities/dashboards",
"l/ro/user-guide/dashboards/capabilities/widgets",
@@ -4132,7 +4132,7 @@
"pages": [
"l/ro/user-guide/permissions-access/overview",
{
"group": "Referință",
"group": "Reference",
"pages": [
"l/ro/user-guide/permissions-access/capabilities/permissions",
"l/ro/user-guide/permissions-access/capabilities/sso-configuration"
@@ -4152,7 +4152,7 @@
"pages": [
"l/ro/user-guide/billing/overview",
{
"group": "Referință",
"group": "Reference",
"pages": [
"l/ro/user-guide/billing/capabilities/pricing-plans",
"l/ro/user-guide/billing/capabilities/credits"
@@ -4172,7 +4172,7 @@
"pages": [
"l/ro/user-guide/settings/overview",
{
"group": "Referință",
"group": "Reference",
"pages": [
"l/ro/user-guide/settings/capabilities/workspace-settings",
"l/ro/user-guide/settings/capabilities/member-management",
@@ -4196,7 +4196,7 @@
"tab": "Dezvoltatori",
"groups": [
{
"group": "Prezentare generală",
"group": "Overview",
"pages": [
"l/ro/developers/introduction"
]
@@ -35,8 +35,14 @@ Everything is detected via AST analysis at build time — no config files, no re
## The developer experience
You write your app as a TypeScript project on your machine. The CLI watches your source files and live-syncs them to a running Twenty server — edit a file, see the change in the UI within a second. The typed API client regenerates automatically when the schema changes. When you're ready, `yarn twenty deploy` pushes to a production server, or `yarn twenty publish` lists your app on npm and the Twenty marketplace.
```bash
npx create-twenty-app@latest my-app
cd my-app
yarn twenty dev
```
`yarn twenty dev` watches your source files, rebuilds on change, and live-syncs to a local Twenty instance. The typed API client regenerates automatically when the schema changes. When you're ready, `yarn twenty deploy` pushes to production. Apps can also be published to npm and listed in the Twenty marketplace.
<Card title="Build your first app" icon="arrow-right" href="/developers/extend/apps/getting-started">
Three-phase walkthrough — scaffold, run a local server, sync your changes.
Full walkthrough — scaffold, develop, deploy.
</Card>
@@ -15,7 +15,7 @@ Front-Komponenten können an zwei Stellen innerhalb von Twenty gerendert werden:
## Einfaches Beispiel
Der schnellste Weg, eine Front-Komponente in Aktion zu sehen, ist, sie als **Befehlsmenüeintrag** zu registrieren. Verwende `defineCommandMenuItem` in einer separaten Datei, damit die Komponente als Schnellaktionsschaltfläche oben rechts auf der Seite erscheint:
Der schnellste Weg, eine Front-Komponente in Aktion zu sehen, ist, sie als Befehl zu registrieren. Das Hinzufügen eines `command`-Felds mit `isPinned: true` lässt sie als Schnellaktionsschaltfläche oben rechts auf der Seite erscheinen — kein Seitenlayout erforderlich:
```tsx src/front-components/hello-world.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
@@ -34,20 +34,14 @@ export default defineFrontComponent({
name: 'hello-world',
description: 'A simple front component',
component: HelloWorld,
});
```
```ts src/command-menu-items/hello-world.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
shortLabel: 'Hello',
label: 'Hello World',
icon: 'IconBolt',
isPinned: true,
availabilityType: 'GLOBAL',
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
command: {
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
shortLabel: 'Hello',
label: 'Hello World',
icon: 'IconBolt',
isPinned: true,
availabilityType: 'GLOBAL',
},
});
```
@@ -61,13 +55,14 @@ Klicken Sie darauf, um die Komponente inline zu rendern.
## Konfigurationsfelder
| Feld | Erforderlich | Beschreibung |
| --------------------- | ------------ | --------------------------------------------------------------------------- |
| `universalIdentifier` | Ja | Stabile eindeutige ID für diese Komponente |
| `component` | Ja | Eine React-Komponentenfunktion |
| `name` | Nein | Anzeigename |
| `description` | Nein | Beschreibung dessen, was die Komponente macht |
| `isHeadless` | Nein | Auf `true` setzen, wenn die Komponente keine sichtbare UI hat (siehe unten) |
| Feld | Erforderlich | Beschreibung |
| --------------------- | ------------ | ---------------------------------------------------------------------------------------- |
| `universalIdentifier` | Ja | Stabile eindeutige ID für diese Komponente |
| `component` | Ja | Eine React-Komponentenfunktion |
| `name` | Nein | Anzeigename |
| `description` | Nein | Beschreibung dessen, was die Komponente macht |
| `isHeadless` | Nein | Auf `true` setzen, wenn die Komponente keine sichtbare UI hat (siehe unten) |
| `command` | Nein | Die Komponente als Befehl registrieren (siehe unten [Befehlsoptionen](#command-options)) |
## Eine Front-Komponente auf einer Seite platzieren
@@ -146,17 +141,11 @@ export default defineFrontComponent({
description: 'Creates a task from the command menu',
component: RunAction,
isHeadless: true,
});
```
```ts src/command-menu-items/run-action.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
icon: 'IconPlayerPlay',
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
command: {
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
icon: 'IconPlayerPlay',
},
});
```
@@ -188,6 +177,11 @@ export default defineFrontComponent({
description: 'Deletes a draft with confirmation',
component: DeleteDraft,
isHeadless: true,
command: {
universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
label: 'Delete draft',
icon: 'IconTrash',
},
});
```
@@ -229,8 +223,7 @@ Verfügbare Hooks:
| Hook | Gibt zurück | Beschreibung |
| --------------------------------------------- | -------------------- | --------------------------------------------------------------------------- |
| `useUserId()` | `string` oder `null` | Die ID des aktuellen Benutzers |
| `useSelectedRecordIds()` | `Zeichenkette[]` | Alle ausgewählten Datensatz-IDs (leeres Array, wenn keine ausgewählt sind) |
| `useRecordId()` | `string` oder `null` | **Veraltet.** Verwenden Sie stattdessen `useSelectedRecordIds()` |
| `useRecordId()` | `string` oder `null` | Die ID des aktuellen Datensatzes (wenn auf einer Datensatzseite platziert) |
| `useFrontComponentId()` | `string` | Die ID dieser Komponenteninstanz |
| `useFrontComponentExecutionContext(selector)` | variiert | Zugriff auf den vollständigen Ausführungskontext mit einer Selektorfunktion |
@@ -293,84 +286,14 @@ export default defineFrontComponent({
});
```
### Mit mehreren Datensätzen arbeiten
## Befehlsoptionen
Verwenden Sie `useSelectedRecordIds()`, um mehrere ausgewählte Datensätze zu verwalten. Dies ist nützlich für Stapelvorgänge:
```tsx src/front-components/bulk-export.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds, numberOfSelectedRecords } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-sdk/clients';
const BulkExport = () => {
const selectedRecordIds = useSelectedRecordIds();
const handleExport = async () => {
const client = new CoreApiClient();
for (const recordId of selectedRecordIds) {
await client.mutation({
updateTask: {
__args: { id: recordId, data: { exported: true } },
id: true,
},
});
}
await enqueueSnackbar({
message: `Exported ${selectedRecordIds.length} records`,
variant: 'success',
});
await closeSidePanel();
};
return (
<div style={{ padding: '20px' }}>
<p>Export {selectedRecordIds.length} selected record(s)?</p>
<button onClick={handleExport}>Export</button>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
name: 'bulk-export',
description: 'Export selected records',
component: BulkExport,
command: {
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902',
label: 'Bulk Export',
availabilityType: 'RECORD_SELECTION',
conditionalAvailabilityExpression: numberOfSelectedRecords > 0,
},
});
```
## defineCommandMenuItem
Verwende `defineCommandMenuItem`, um eine Front-Komponente im Befehlsmenü (Cmd+K) zu registrieren. Wenn `isPinned` `true` ist, erscheint sie außerdem als Schnellaktionsschaltfläche oben rechts auf der Seite.
```ts src/command-menu-items/open-dashboard.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
label: 'Open Dashboard',
shortLabel: 'Dashboard',
icon: 'IconLayoutDashboard',
isPinned: true,
availabilityType: 'GLOBAL',
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
});
```
Das Hinzufügen eines `command`-Felds zu `defineFrontComponent` registriert die Komponente im Befehlsmenü (Cmd+K). Wenn `isPinned` `true` ist, erscheint sie außerdem als Schnellaktionsschaltfläche oben rechts auf der Seite.
| Feld | Erforderlich | Beschreibung |
| --------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `universalIdentifier` | Ja | Stabile eindeutige ID für den Befehl |
| `label` | Ja | Vollständiges Label, das im Befehlsmenü (Cmd+K) angezeigt wird |
| `frontComponentUniversalIdentifier` | Ja | Der `universalIdentifier` der Front-Komponente, die dieser Befehl öffnet |
| `shortLabel` | Nein | Kürzeres Label, das auf der angehefteten Schnellaktionsschaltfläche angezeigt wird |
| `icon` | Nein | Neben dem Label angezeigter Icon-Name (z. B. 'IconBolt', 'IconSend') |
| `isPinned` | Nein | Bei `true` wird der Befehl als Schnellaktionsschaltfläche oben rechts auf der Seite angezeigt |
@@ -382,23 +305,30 @@ export default defineCommandMenuItem({
Mit dem Feld `conditionalAvailabilityExpression` können Sie basierend auf dem aktuellen Seitenkontext steuern, wann ein Befehl sichtbar ist. Importieren Sie typisierte Variablen und Operatoren aus `twenty-sdk`, um Ausdrücke zu erstellen:
```ts src/command-menu-items/bulk-update.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
```tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import {
pageType,
numberOfSelectedRecords,
objectPermissions,
everyEquals,
isDefined,
} from 'twenty-sdk/front-component';
export default defineCommandMenuItem({
export default defineFrontComponent({
universalIdentifier: '...',
label: 'Bulk Update',
availabilityType: 'RECORD_SELECTION',
frontComponentUniversalIdentifier: '...',
conditionalAvailabilityExpression: everyEquals(
objectPermissions,
'canUpdateObjectRecords',
true,
),
name: 'bulk-action',
component: BulkAction,
command: {
universalIdentifier: '...',
label: 'Bulk Update',
availabilityType: 'RECORD_SELECTION',
conditionalAvailabilityExpression: everyEquals(
objectPermissions,
'canUpdateObjectRecords',
true,
),
},
});
```
@@ -4,52 +4,48 @@ icon: rocket
description: Erstellen Sie in wenigen Minuten Ihre erste Twenty-App.
---
## Was sind Apps?
Apps ermöglichen es Ihnen, Twenty mit benutzerdefinierten Objekten, Feldern, Logikfunktionen, Frontend-Komponenten, KI-Fähigkeiten und mehr zu erweitern — alles als Code verwaltet. Anstatt alles über die UI zu konfigurieren, definieren Sie Ihr Datenmodell und Ihre Logik in TypeScript und stellen es in einem oder mehreren Workspaces bereit.
## Voraussetzungen
Bevor Sie beginnen, stellen Sie sicher, dass Folgendes auf Ihrem Rechner installiert ist:
* **Node.js 24+** — [Hier herunterladen](https://nodejs.org/)
* **Yarn 4** — Wird mit Node.js über Corepack mitgeliefert. Aktivieren Sie es: `corepack enable`
* **Docker** — [Hier herunterladen](https://www.docker.com/products/docker-desktop/). Erforderlich, um einen lokalen Twenty-Server auszuführen. Überspringen Sie dies, wenn Twenty bereits anderswo läuft.
* **Yarn 4** — Wird mit Node.js über Corepack mitgeliefert. Aktivieren Sie es, indem Sie `corepack enable` ausführen
* **Docker** — [Hier herunterladen](https://www.docker.com/products/docker-desktop/). Erforderlich, um eine lokale Twenty-Instanz auszuführen. Nicht erforderlich, wenn bereits ein Twenty-Server läuft.
Das Erstellen einer Twenty-App umfasst drei Phasen. Das Scaffolding-Tool fasst sie zu einem einzigen Happy-Path-Befehl zusammen, aber jede Phase ist ein eigenes Konzept — wenn etwas fehlschlägt, hilft Ihnen das Wissen, in welcher Phase Sie sich befinden, zu erkennen, was zu beheben ist.
## Erstellen Sie Ihre erste App
| Phase | Was Sie tun | Tool | Ergebnis |
| ----------------------- | ------------------------------------------------------- | ----------------------------- | ---------------------------------------------------- |
| **1. Gerüst erstellen** | Den Quellcode der App erzeugen | `npx create-twenty-app` | Ein TypeScript-Projekt auf der Festplatte |
| **2. Server starten** | Einen Twenty-Server starten, in den synchronisiert wird | Docker + `yarn twenty server` | Eine laufende Twenty-Instanz |
| **3. Synchronisieren** | Ihren Code live mit dem Server synchronisieren | `yarn twenty dev` | Ihre Änderungen erscheinen in der Benutzeroberfläche |
### App-Gerüst erstellen
---
## Phase 1 — Projektgerüst erstellen
Erstellen Sie eine neue App aus der Vorlage:
Öffnen Sie ein Terminal und führen Sie Folgendes aus:
```bash filename="Terminal"
npx create-twenty-app@latest my-twenty-app
```
Sie werden nach einem Namen und einer Beschreibung gefragt — drücken Sie **Enter** für die Standardwerte. Dadurch wird ein TypeScript-Projekt in `my-twenty-app/` erzeugt, mit einer Startdatei `application-config.ts`, einer Standardrolle, einem CI-Workflow und einem Integrationstest.
Sie werden aufgefordert, einen Namen und eine Beschreibung für Ihre App einzugeben. Drücken Sie **Enter**, um die Standardwerte zu übernehmen.
**Nach dieser Phase:** Sie haben den Quellcode einer App auf Ihrem Rechner. Es läuft noch nicht — das ist Phase 2.
Dadurch wird ein neuer Ordner namens `my-twenty-app` mit allem erstellt, was Sie benötigen.
---
### Lokale Twenty-Instanz einrichten
## Phase 2 — Einen lokalen Twenty-Server starten
Ihre App benötigt einen Twenty-Server, in den sie synchronisieren kann. Der Server ist eine vollständige Twenty-Instanz — UI, GraphQL-API, PostgreSQL — die lokal in Docker läuft. Ihr lokaler Code lädt seine Definitionen auf diesen Server hoch, wodurch sie in der Benutzeroberfläche erscheinen.
Das Scaffolding-Tool bietet an, einen für Sie zu starten:
Das Scaffolding-Tool fragt:
> **Möchten Sie eine lokale Twenty-Instanz einrichten?**
* **Ja (empfohlen)**lädt das Docker-Image `twentycrm/twenty-app-dev` herunter und startet es auf Port `2020`. Stellen Sie sicher, dass Docker läuft.
* **Nein** — wählen Sie dies, wenn Sie bereits einen Twenty-Server haben, mit dem Sie sich verbinden möchten. Sie können die Verbindung später mit `yarn twenty remote add` herstellen.
* **Geben Sie `yes` ein** (empfohlen) — Dadurch wird das Docker-Image `twenty-app-dev` heruntergeladen und ein lokaler Twenty-Server auf Port `2020` gestartet. Stellen Sie sicher, dass Docker läuft, bevor Sie fortfahren.
* **Geben Sie `no` ein** — Wählen Sie dies, wenn bereits ein Twenty-Server lokal läuft.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/start-instance.png" alt="Soll die lokale Instanz gestartet werden?" />
</div>
Sobald der Server läuft, öffnet sich ein Browser zur Anmeldung. Verwenden Sie das vorab eingerichtete Demo-Konto:
### Melden Sie sich bei Ihrem Arbeitsbereich an
Anschließend öffnet sich ein Browserfenster mit der Twenty-Anmeldeseite. Melden Sie sich mit dem vorab eingerichteten Demo-Konto an:
* **E-Mail:** `tim@apple.dev`
* **Passwort:** `tim@apple.dev`
@@ -58,81 +54,83 @@ Sobald der Server läuft, öffnet sich ein Browser zur Anmeldung. Verwenden Sie
<img src="/images/docs/developers/extends/apps/login.png" alt="Twenty-Anmeldebildschirm" />
</div>
Klicken Sie auf dem nächsten Bildschirm auf **Authorize** — dadurch erhält die CLI Zugriff auf Ihren Arbeitsbereich.
### Autorisieren Sie die App
Nach der Anmeldung sehen Sie einen Autorisierungsbildschirm. Dadurch kann Ihre App mit Ihrem Arbeitsbereich interagieren.
Klicken Sie auf **Authorize**, um fortzufahren.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/authorize.png" alt="Twenty-CLI-Autorisierungsbildschirm" />
</div>
Ihr Terminal bestätigt, dass alles eingerichtet ist.
Nach der Autorisierung bestätigt Ihr Terminal, dass alles eingerichtet ist.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/scaffolded.png" alt="App-Gerüst erfolgreich erstellt" />
</div>
**Nach dieser Phase:** Sie haben einen laufenden Twenty-Server unter [http://localhost:2020](http://localhost:2020), und Ihre CLI ist autorisiert, mit ihm zu synchronisieren.
### Beginnen Sie mit der Entwicklung
<Note>
Wenn Docker nicht installiert ist oder nicht läuft, zeigt das Scaffolding-Tool den richtigen Startbefehl für Ihr Betriebssystem an. Sobald Docker läuft, können Sie mit `yarn twenty server start` fortfahren — ein erneutes Scaffolding ist nicht nötig.
</Note>
---
## Phase 3 — Ihre Änderungen synchronisieren
Das ist die innere Schleife, in der Sie die meiste Zeit verbringen werden.
Wechseln Sie in Ihren neuen App-Ordner und starten Sie den Entwicklungsserver:
```bash filename="Terminal"
cd my-twenty-app
yarn twenty dev
```
Dies überwacht `src/`, baut bei jeder Änderung neu und synchronisiert das Ergebnis mit dem Server. Bearbeiten Sie eine Datei, speichern Sie, und innerhalb einer Sekunde spiegelt der Server die Änderung wider. Sie sehen eine Live-Statusanzeige in Ihrem Terminal.
Dadurch werden Ihre Quelldateien überwacht, bei jeder Änderung neu gebaut und Ihre App automatisch mit dem lokalen Twenty-Server synchronisiert. In Ihrem Terminal sollte eine Live-Statusanzeige angezeigt werden.
Für ausführlichere Ausgaben (Build-Protokolle, Sync-Anfragen, Fehlerspuren) fügen Sie `--verbose` hinzu.
Für ausführlichere Ausgaben (Build-Protokolle, Sync-Anfragen, Fehlerspuren) verwenden Sie das Flag `--verbose`:
```bash filename="Terminal"
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`, 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.png" alt="Terminalausgabe im Dev-Modus" />
</div>
Öffnen Sie [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer). Unter **Your Apps** sollte Ihre App angezeigt werden.
#### Einmalige Synchronisierung mit `yarn twenty dev --once`
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-1.png" alt="Liste &#x22;Your Apps&#x22;, die &#x22;My twenty app&#x22; anzeigt" />
</div>
Klicken Sie auf **My twenty app**, um die **Anwendungsregistrierung** anzuzeigen — ein serverseitiger Datensatz, der Ihre App beschreibt (Name, Bezeichner, OAuth-Anmeldedaten, Quelle). Eine Registrierung kann in mehreren Arbeitsbereichen auf demselben Server installiert werden.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-2.png" alt="Details der Anwendungsregistrierung" />
</div>
Klicken Sie auf **View installed app**, um die Installation im Arbeitsbereich anzuzeigen. Die Registerkarte **About** zeigt die Version und Verwaltungsoptionen.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Installierte App" />
</div>
**Nach dieser Phase:** Sie haben eine Live-Entwicklungsschleife. Bearbeiten Sie eine beliebige Datei in `src/`, und sie erscheint in der Benutzeroberfläche.
### Einmalige Synchronisierung für CI und Skripte
Verwenden Sie `--once`, um einen einzelnen Build + Sync auszuführen und zu beenden — gleiche Pipeline, kein Watcher:
Wenn Sie keinen Watcher im Hintergrund ausführen lassen möchten (zum Beispiel in einer CI-Pipeline, einem Git-Hook oder einem skriptgesteuerten Workflow), geben Sie das Flag `--once` an. Es führt dieselbe Pipeline aus wie `yarn twenty dev` — Build-Manifest erstellen, Dateien bündeln, hochladen, synchronisieren, den typisierten API-Client neu generieren — beendet sich jedoch, **sobald die Synchronisierung abgeschlossen ist**:
```bash filename="Terminal"
yarn twenty dev --once
```
| Befehl | Verhalten | Wann verwenden |
| ------------------------ | ---------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| `yarn twenty dev` | Überwacht und synchronisiert bei jeder Änderung erneut. Läuft, bis Sie es stoppen. | Interaktive lokale Entwicklung. |
| `yarn twenty dev --once` | Einmaliger Build + Sync, beendet sich mit `0` bei Erfolg, mit `1` bei Fehler. | CI, Pre-Commit-Hooks, KI-Agenten, skriptgesteuerte Workflows. |
| Befehl | Verhalten | Wann verwenden |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `yarn twenty dev` | Überwacht Ihre Quelldateien und synchronisiert bei jeder Änderung erneut. Läuft weiter, bis Sie es stoppen. | Interaktive lokale Entwicklung — Sie möchten das Live-Status-Panel und eine sofortige Feedback-Schleife. |
| `yarn twenty dev --once` | Führt einen einzelnen Build + Sync aus und beendet sich anschließend mit Code `0` bei Erfolg oder `1` bei einem Fehler. | Skripte, CI, Pre-Commit-Hooks, KI-Agenten und jeder nicht-interaktive Workflow. |
Beide Modi benötigen einen Server im Entwicklungsmodus und eine authentifizierte Remote-Verbindung.
Beide Modi erfordern einen Twenty-Server, der im Entwicklungsmodus läuft, und ein authentifiziertes Remote — es gelten dieselben Voraussetzungen.
<Warning>
Der Dev-Modus ist nur auf Twenty-Instanzen verfügbar, die im Entwicklungsmodus laufen (`NODE_ENV=development`). Produktionsinstanzen lehnen Dev-Sync-Anfragen ab — verwenden Sie `yarn twenty deploy`, um auf Produktionsserver bereitzustellen. Siehe [Apps veröffentlichen](/l/de/developers/extend/apps/publishing).
</Warning>
### Sehen Sie sich Ihre App in Twenty an
Öffnen Sie [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer) in Ihrem Browser. Navigieren Sie zu **Settings > Apps** und wählen Sie die Registerkarte **Developer**. Unter **Your Apps** sollte Ihre App aufgeführt sein:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-1.png" alt="Liste &#x22;Your Apps&#x22;, die &#x22;My twenty app&#x22; anzeigt" />
</div>
Klicken Sie auf **My twenty app**, um die **Anwendungsregistrierung** zu öffnen. Eine Registrierung ist ein Servereintrag, der Ihre App beschreibt — ihren Namen, den eindeutigen Bezeichner, OAuth-Zugangsdaten und die Quelle (lokal, npm oder Tarball). Sie befindet sich auf dem Server, nicht in einem bestimmten Arbeitsbereich. Wenn Sie eine App in einen Arbeitsbereich installieren, erstellt Twenty eine arbeitsbereichsbezogene Anwendung, die auf diese Registrierung verweist. Eine Registrierung kann in mehreren Arbeitsbereichen auf demselben Server installiert werden.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-2.png" alt="Details der Anwendungsregistrierung" />
</div>
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" />
</div>
Alles erledigt! Bearbeiten Sie eine beliebige Datei in `src/`, und die Änderungen werden automatisch übernommen.
---
@@ -140,112 +138,136 @@ Der Dev-Modus ist nur auf Twenty-Instanzen verfügbar, die im Entwicklungsmodus
Apps bestehen aus **Entitäten** — jede ist als TypeScript-Datei mit einem einzigen `export default` definiert:
| Entität | Was sie macht |
| -------------------------- | ------------------------------------------------------------------------------------------------- |
| **Objekte & Felder** | Benutzerdefinierte Datenmodelle (Postkarte, Rechnung usw.) mit typisierten Feldern |
| **Logikfunktionen** | Serverseitiges TypeScript, ausgelöst durch HTTP-Routen, Cron-Zeitpläne oder Datenbankereignisse |
| **Frontend-Komponenten** | React-Komponenten, die in der UI von Twenty gerendert werden (Seitenleiste, Widgets, Befehlsmenü) |
| **Fähigkeiten & Agenten** | KI-Funktionen — wiederverwendbare Anweisungen und autonome Assistenten |
| **Ansichten & Navigation** | Vorkonfigurierte Listenansichten und Seitenleisteneinträge |
| **Seitenlayouts** | Benutzerdefinierte Datensatz-Detailseiten mit Tabs und Widgets |
| Entität | Was sie macht |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| **Objekte & Felder** | Definieren Sie benutzerdefinierte Datenmodelle (wie Post Card, Invoice) mit typisierten Feldern |
| **Logikfunktionen** | Serverseitige TypeScript-Funktionen, die durch HTTP-Routen, Cron-Zeitpläne oder Datenbankereignisse ausgelöst werden |
| **Frontend-Komponenten** | React-Komponenten, die in der UI von Twenty gerendert werden (Seitenleiste, Widgets, Befehlsmenü) |
| **Fähigkeiten & Agenten** | KI-Funktionen — wiederverwendbare Anweisungen und autonome Assistenten |
| **Ansichten & Navigation** | Vorkonfigurierte Listenansichten und Seitenleisteneinträge für Ihre Objekte |
| **Seitenlayouts** | Benutzerdefinierte Datensatz-Detailseiten mit Tabs und Widgets |
Vollständige Referenz: [Apps entwickeln](/l/de/developers/extend/apps/building).
Wechseln Sie zu [Apps erstellen](/l/de/developers/extend/apps/building) für eine ausführliche Anleitung zu jedem Entitätstyp.
---
## Projektstruktur
Der Scaffolder erzeugt die folgende Verzeichnisstruktur:
```text filename="my-twenty-app/"
my-twenty-app/
package.json
yarn.lock
.gitignore
.nvmrc
.yarnrc.yml
.oxlintrc.json
tsconfig.json
tsconfig.spec.json # TypeScript config for tests
vitest.config.ts # Vitest test runner configuration
LLMS.md
README.md
.github/
└── workflows/
└── ci.yml # GitHub Actions CI workflow
public/ # Public assets (images, fonts, etc.)
src/
application-config.ts # Required — your app's entry point
default-role.ts # Permissions for logic functions
constants/
universal-identifiers.ts # Auto-generated UUIDs and metadata
__tests__/
setup-test.ts
app-install.integration-test.ts
.github/workflows/ci.yml # GitHub Actions
public/ # Static assets
vitest.config.ts # Test runner config
tsconfig.json, tsconfig.spec.json
.nvmrc, .yarnrc.yml, .oxlintrc.json
README.md, LLMS.md
├── application-config.ts # Required — main application configuration
├── default-role.ts # Default role for logic functions
├── constants/
└── universal-identifiers.ts # Auto-generated UUIDs and app metadata
└── __tests__/
├── setup-test.ts # Test setup (server health check, config)
└── app-install.integration-test.ts # Integration test
```
| Datei / Ordner | Zweck |
| ---------------------------------------- | ------------------------------------------------------------------------------- |
| `src/application-config.ts` | **Erforderlich.** Die Hauptkonfigurationsdatei für Ihre App. |
| `src/default-role.ts` | Standardrolle, die steuert, worauf Ihre Logikfunktionen zugreifen können. |
| `src/constants/universal-identifiers.ts` | Automatisch erzeugte UUIDs und Metadaten (Anzeigename, Beschreibung). |
| `src/__tests__/` | Integrationstests (Setup + Beispieltest). |
| `public/` | Statische Assets (Bilder, Schriftarten), die mit Ihrer App ausgeliefert werden. |
### Mit einem Beispiel beginnen
Verwenden Sie `--example`, um mit einem vollständigeren Projekt zu starten (benutzerdefinierte Objekte, Felder, Logikfunktionen, Front-End-Komponenten):
Um mit einem umfassenderen Beispiel mit benutzerdefinierten Objekten, Feldern, Logikfunktionen, Frontend-Komponenten und mehr zu starten, verwenden Sie die Option `--example`:
```bash filename="Terminal"
npx create-twenty-app@latest my-twenty-app --example postcard
```
Die Beispiele befinden sich unter [twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples). Sie können auch einzelne Entitäten in einem bestehenden Projekt mit `yarn twenty add` erzeugen siehe [Apps entwickeln](/l/de/developers/extend/apps/building#scaffolding-entities-with-yarn-twenty-add).
Die Beispiele stammen aus dem Verzeichnis [twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples) auf GitHub. Sie können auch einzelne Entitäten in einem bestehenden Projekt mit `yarn twenty add` erzeugen (siehe [Apps erstellen](/l/de/developers/extend/apps/building#scaffolding-entities-with-yarn-twenty-add)).
---
### Wichtige Dateien
## Lokalen Server verwalten
| Datei / Ordner | Zweck |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `package.json` | Deklariert den App-Namen, die Version und Abhängigkeiten. Enthält ein `twenty`-Skript, sodass Sie `yarn twenty help` ausführen können, um alle Befehle anzuzeigen. |
| `src/application-config.ts` | **Erforderlich.** Die Hauptkonfigurationsdatei für Ihre App. |
| `src/default-role.ts` | Standardrolle, die steuert, worauf Ihre Logikfunktionen zugreifen können. |
| `src/constants/universal-identifiers.ts` | Automatisch erzeugte UUIDs und App-Metadaten (Anzeigename, Beschreibung). |
| `src/__tests__/` | Integrationstests (Setup + Beispieltest). |
| `public/` | Statische Assets (Bilder, Schriftarten), die mit Ihrer App ausgeliefert werden. |
Verwenden Sie `yarn twenty server`, um den lokalen Twenty-Container zu steuern:
## Lokaler Entwicklungsserver
| Befehl | Was es tut |
| -------------------------------------- | --------------------------------------------------- |
| `yarn twenty server start` | Server starten (lädt das Image bei Bedarf herunter) |
| `yarn twenty server start --port 3030` | Auf einem benutzerdefinierten Port starten |
| `yarn twenty server stop` | Server stoppen (Daten bleiben erhalten) |
| `yarn twenty server status` | URL, Version und Anmeldedaten anzeigen |
| `yarn twenty server logs` | Serverprotokolle streamen |
| `yarn twenty server reset` | Alle Daten löschen und neu starten |
| `yarn twenty server upgrade` | Das neueste `twenty-app-dev`-Image herunterladen |
| `yarn twenty server upgrade 2.2.0` | Auf eine bestimmte Version aktualisieren |
Der Scaffolder hat bereits einen lokalen Twenty-Server für Sie gestartet. Um ihn später zu verwalten, verwenden Sie `yarn twenty server`:
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.
| 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
`yarn twenty server upgrade` lädt das neueste Image herunter, vergleicht die Digests und erstellt den Container nur neu, wenn sich tatsächlich etwas geändert hat. Die Volumes bleiben erhalten — nur der Container wird ersetzt. Wenn ein neues Image heruntergeladen wurde und der Container lief, startet das Upgrade automatisch einen neuen Container; führen Sie anschließend `yarn twenty server start` aus, um zu warten, bis er betriebsbereit ist.
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"
yarn twenty server upgrade # Latest
yarn twenty server upgrade 2.2.0 # Specific version
# 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
```
Überprüfen Sie die laufende Version mit `yarn twenty server status` (dies zeigt die im Container enthaltene `APP_VERSION` an).
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.
### Eine parallele Testinstanz ausführen
Sie können die laufende Version mit `yarn twenty server status` überprüfen; dieser Befehl zeigt die `APP_VERSION` des Containers an.
Übergeben Sie `--test` an jeden `server`-Befehl, um eine zweite, vollständig isolierte Instanz zu verwalten — nützlich für Integrationstests oder Experimente, ohne Ihre Hauptentwicklungsdaten anzutasten:
### Eine Testinstanz ausführen
| Befehl | Was es tut |
| ----------------------------------- | ------------------------------------------------- |
| `yarn twenty server start --test` | Die Testinstanz starten (standardmäßig Port 2021) |
| `yarn twenty server stop --test` | Anhalten |
| `yarn twenty server status --test` | Status anzeigen |
| `yarn twenty server logs --test` | Protokolle streamen |
| `yarn twenty server reset --test` | Daten löschen |
| `yarn twenty server upgrade --test` | Image aktualisieren |
Ü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.
Die Testinstanz hat ihren eigenen Container (`twenty-app-dev-test`), eigene Volumes (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`) und eine eigene Konfiguration — sie läuft parallel zu Ihrer Hauptinstanz ohne Konflikte. Kombinieren Sie `--test` mit `--port`, um den Port 2021 zu überschreiben.
| 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.
<Note>
Der Server erfordert, dass **Docker** läuft. Wenn der Fehler "Docker not running" angezeigt wird, stellen Sie sicher, dass Docker Desktop (oder der Docker-Daemon) gestartet ist.
</Note>
## Manuelle Einrichtung (ohne Scaffolder)
Überspringen Sie das Scaffolding-Tool, wenn Sie das SDK zu einem bestehenden Projekt hinzufügen:
Wenn Sie die Einrichtung lieber selbst vornehmen möchten, anstatt `create-twenty-app` zu verwenden, können Sie dies in zwei Schritten tun.
**1. Fügen Sie `twenty-sdk` und `twenty-client-sdk` als Abhängigkeiten hinzu:**
```bash filename="Terminal"
yarn add twenty-sdk twenty-client-sdk
```
Fügen Sie der `package.json` das Skript hinzu:
**2. Fügen Sie Ihrer `package.json` ein `twenty`-Skript hinzu:**
```json filename="package.json"
{
@@ -255,19 +277,19 @@ Fügen Sie der `package.json` das Skript hinzu:
}
```
Sie können jetzt `yarn twenty dev`, `yarn twenty server start` und den Rest ausführen.
Sie können jetzt `yarn twenty dev`, `yarn twenty help` und alle anderen Befehle ausführen.
<Note>
Installieren Sie `twenty-sdk` nicht global — fixieren Sie es pro Projekt, damit jede App ihre eigene Version verwendet.
Installieren Sie `twenty-sdk` nicht global. Verwenden Sie es immer als lokale Projektabhängigkeit, damit jedes Projekt seine eigene Version festlegen kann.
</Note>
---
## Fehlerbehebung
* **Docker-Fehler** — Stellen Sie sicher, dass Docker Desktop (oder der Daemon) läuft, bevor Sie `yarn twenty server start` ausführen. Die Fehlermeldung zeigt den richtigen Startbefehl für Ihr Betriebssystem an.
* **Falsche Node-Version** — 24+ erforderlich. Prüfen Sie mit `node -v`.
* **Yarn 4 fehlt** — Führen Sie `corepack enable` aus.
* **Abhängigkeiten defekt** — `rm -rf node_modules && yarn install`.
Wenn Probleme auftreten:
Hängen Sie fest? Bitten Sie im [Twenty-Discord](https://discord.com/channels/1130383047699738754/1130386664812982322) um Hilfe.
* Stellen Sie sicher, dass **Docker läuft**, bevor Sie das Scaffolding-Tool mit einer lokalen Instanz starten.
* Stellen Sie sicher, dass Sie **Node.js 24+** verwenden (`node -v` zur Überprüfung).
* Stellen Sie sicher, dass **Corepack aktiviert ist** (`corepack enable`), damit Yarn 4 verfügbar ist.
* Versuchen Sie, `node_modules` zu löschen und `yarn install` erneut auszuführen, wenn Abhängigkeiten fehlerhaft erscheinen.
Hängen Sie immer noch fest? Bitten Sie im [Twenty-Discord](https://discord.com/channels/1130383047699738754/1130386664812982322) um Hilfe.
@@ -141,14 +141,11 @@ const handler = async (event: RoutePayload) => {
Header-Namen werden in Kleinbuchstaben normalisiert. Greifen Sie mit Schlüsseln in Kleinbuchstaben darauf zu (z. B. `event.headers['content-type']`).
</Note>
#### Eine Funktion als KI-Tool oder Workflow-Aktion verfügbar machen
#### Eine Funktion als Tool bereitstellen
Logikfunktionen können auf zwei Oberflächen verfügbar gemacht werden, jeweils mit eigenem Trigger:
Logikfunktionen können als **Tools** für KI-Agenten und Workflows verfügbar gemacht werden. Wenn eine Funktion als Tool markiert ist, wird sie von den KI-Funktionen von Twenty auffindbar und kann in Workflow-Automatisierungen verwendet werden.
* **`toolTriggerSettings`** — macht die Funktion über die KI-Funktionen von Twenty (Chat, MCP, Funktionsaufrufe) auffindbar. Verwendet das standardmäßige JSON Schema, das Format, das LLMs nativ verstehen.
* **`workflowActionTriggerSettings`** — lässt die Funktion als Schritt im visuellen Workflow-Builder erscheinen. Verwendet das umfangreiche `InputSchema` von Twenty, sodass der Builder geeignete Feldeditoren, Variablenauswahlen und Beschriftungen rendern kann.
Eine Funktion kann sich für eine, die andere oder beide entscheiden. Sie stehen neben `cronTriggerSettings`, `databaseEventTriggerSettings` und `httpRouteTriggerSettings` — gleiches Muster, gleiche Struktur.
Um eine Logikfunktion als Tool zu markieren, setzen Sie `isTool: true`:
```ts src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
@@ -178,33 +175,31 @@ export default defineLogicFunction({
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
toolTriggerSettings: {},
isTool: true,
});
```
Hauptpunkte:
* Eine Funktion kann Oberflächen mischen — deklarieren Sie sowohl `toolTriggerSettings` als auch `workflowActionTriggerSettings`, um sie im Chat UND im Workflow-Builder bereitzustellen.
* `toolTriggerSettings.inputSchema` und `workflowActionTriggerSettings.inputSchema` sind beide optional. Wenn sie weggelassen werden, leitet der Manifest-Builder sie aus dem Handler-Quellcode ab (JSON Schema für das KI-Tool, das `InputSchema` von Twenty für die Workflow-Aktion). Geben Sie eines explizit an, wenn Sie eine reichere Typisierung wünschen — zum Beispiel mit `FieldMetadataType`-fähigen Feldern wie `CURRENCY` oder `RELATION` für den Workflow-Builder oder mit `description`-Feldern, die der KI-Agent lesen kann:
* Sie können `isTool` mit Triggern kombinieren — eine Funktion kann gleichzeitig sowohl ein Tool (von KI-Agenten aufrufbar) als auch durch Ereignisse ausgelöst werden.
* **`toolInputSchema`** (optional): Ein JSON-Schema-Objekt, das die Parameter beschreibt, die Ihre Funktion akzeptiert. Das Schema wird automatisch durch statische Analyse des Quellcodes ermittelt, Sie können es jedoch auch explizit festlegen:
```ts
export default defineLogicFunction({
...,
toolTriggerSettings: {
inputSchema: {
type: 'object',
properties: {
companyName: {
type: 'string',
description: 'The name of the company to enrich',
},
domain: {
type: 'string',
description: 'The company website domain (optional)',
},
toolInputSchema: {
type: 'object',
properties: {
companyName: {
type: 'string',
description: 'The name of the company to enrich',
},
domain: {
type: 'string',
description: 'The company website domain (optional)',
},
required: ['companyName'],
},
required: ['companyName'],
},
});
```
@@ -243,7 +238,7 @@ yarn twenty exec --postInstall
```
Hauptpunkte:
* Post-Installationsfunktionen verwenden `definePostInstallLogicFunction()` — eine spezialisierte Variante, die Trigger-Einstellungen (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `toolTriggerSettings`, `workflowActionTriggerSettings`) weglässt.
* Post-Installationsfunktionen verwenden `definePostInstallLogicFunction()` — eine spezialisierte Variante, die Trigger-Einstellungen (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`) weglässt.
* Der Handler erhält ein `InstallPayload` mit `{ previousVersion?: string; newVersion: string }` — `newVersion` ist die zu installierende Version, und `previousVersion` ist die zuvor installierte Version (oder `undefined` bei einer Neuinstallation). Verwenden Sie diese Werte, um Neuinstallationen von Upgrades zu unterscheiden und versionsspezifische Migrationslogik auszuführen.
* **Wann der Hook ausgeführt wird**: standardmäßig nur bei Neuinstallationen. Übergeben Sie `shouldRunOnVersionUpgrade: true`, wenn er auch beim Upgrade der App von einer vorherigen Version ausgeführt werden soll. Wenn weggelassen, ist das Flag standardmäßig `false` und Upgrades überspringen den Hook.
* **Ausführungsmodell — standardmäßig asynchron, synchron optional**: Das Flag `shouldRunSynchronously` steuert, *wie* Post-Install ausgeführt wird.
@@ -198,14 +198,6 @@ export default defineApplication({
Siehe das [defineApplication-Akkordeon](/l/de/developers/extend/apps/building#defineentity-functions) auf der Seite Building Apps für die vollständige Liste der Marktplatzfelder (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl` usw.).
#### Empfohlene Abmessungen für Screenshots
Der Marktplatz stellt `screenshots` in einem festen `8:5`-Container dar (zum Beispiel `1600×1000 px`).
<Note>
Screenshots mit beliebigem Seitenverhältnis werden vollständig angezeigt und niemals beschnitten, aber alles, was deutlich höher oder schmaler als `8:5` ist, zeigt an den Seiten leere Balken.
</Note>
### Veröffentlichen
```bash filename="Terminal"
@@ -225,9 +217,9 @@ Der Twenty-Server synchronisiert seinen Marktplatzkatalog **stündlich** aus der
Sie können die Synchronisierung sofort auslösen, anstatt zu warten:
```bash filename="Terminal"
yarn twenty server catalog-sync
yarn twenty catalog-sync
# To target a specific remote:
# yarn twenty server catalog-sync --remote production
# yarn twenty catalog-sync --remote production
```
Die im Marktplatz angezeigten Metadaten stammen aus Ihrer `defineApplication()`-Konfiguration — Felder wie `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl` und `termsUrl`.
@@ -15,7 +15,7 @@ Os componentes de front-end podem ser renderizados em dois locais dentro do Twen
## Exemplo básico
A maneira mais rápida de ver um componente de front-end em ação é registrá-lo como um **item do menu de comando**. Use `defineCommandMenuItem` em um arquivo separado para fazer o componente aparecer como um botão de ação rápida no canto superior direito da página:
A maneira mais rápida de ver um componente de front-end em ação é registrá-lo como um **comando**. Adicionar um campo `command` com `isPinned: true` faz com que ele apareça como um botão de ação rápida no canto superior direito da página — não é necessário layout de página:
```tsx src/front-components/hello-world.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
@@ -34,20 +34,14 @@ export default defineFrontComponent({
name: 'hello-world',
description: 'A simple front component',
component: HelloWorld,
});
```
```ts src/command-menu-items/hello-world.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
shortLabel: 'Hello',
label: 'Hello World',
icon: 'IconBolt',
isPinned: true,
availabilityType: 'GLOBAL',
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
command: {
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
shortLabel: 'Hello',
label: 'Hello World',
icon: 'IconBolt',
isPinned: true,
availabilityType: 'GLOBAL',
},
});
```
@@ -61,13 +55,14 @@ Clique nele para renderizar o componente inline.
## Campos de configuração
| Campo | Obrigatório | Descrição |
| --------------------- | ----------- | ---------------------------------------------------------------------------- |
| `universalIdentifier` | Sim | ID único e estável para este componente |
| `component` | Sim | Uma função de componente React |
| `name` | Não | Nome de Exibição |
| `description` | Não | Descrição do que o componente faz |
| `isHeadless` | Não | Defina como `true` se o componente não tiver interface visível (veja abaixo) |
| Campo | Obrigatório | Descrição |
| --------------------- | ----------- | ----------------------------------------------------------------------------------------- |
| `universalIdentifier` | Sim | ID único e estável para este componente |
| `component` | Sim | Uma função de componente React |
| `name` | Não | Nome de Exibição |
| `description` | Não | Descrição do que o componente faz |
| `isHeadless` | Não | Defina como `true` se o componente não tiver interface visível (veja abaixo) |
| `command` | Não | Registre o componente como um comando (veja [opções de comando](#command-options) abaixo) |
## Colocando um componente de front-end em uma página
@@ -146,17 +141,11 @@ export default defineFrontComponent({
description: 'Creates a task from the command menu',
component: RunAction,
isHeadless: true,
});
```
```ts src/command-menu-items/run-action.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
icon: 'IconPlayerPlay',
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
command: {
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
icon: 'IconPlayerPlay',
},
});
```
@@ -188,6 +177,11 @@ export default defineFrontComponent({
description: 'Deletes a draft with confirmation',
component: DeleteDraft,
isHeadless: true,
command: {
universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
label: 'Delete draft',
icon: 'IconTrash',
},
});
```
@@ -226,13 +220,12 @@ export default defineFrontComponent({
Hooks disponíveis:
| Hook | Retorna | Descrição |
| --------------------------------------------- | ------------------ | ----------------------------------------------------------------------------------- |
| `useUserId()` | `string` ou `null` | O ID do usuário atual |
| `useSelectedRecordIds()` | `string[]` | Todos os IDs dos registros selecionados (array vazio se nenhum estiver selecionado) |
| `useRecordId()` | `string` ou `null` | **Obsoleto.** Use `useSelectedRecordIds()` em vez disso |
| `useFrontComponentId()` | `string` | O ID desta instância do componente |
| `useFrontComponentExecutionContext(selector)` | varia | Acesse o contexto de execução completo com uma função seletora |
| Hook | Retorna | Descrição |
| --------------------------------------------- | ------------------ | ------------------------------------------------------------------ |
| `useUserId()` | `string` ou `null` | O ID do usuário atual |
| `useRecordId()` | `string` ou `null` | O ID do registro atual (quando colocado em uma página de registro) |
| `useFrontComponentId()` | `string` | O ID desta instância do componente |
| `useFrontComponentExecutionContext(selector)` | varia | Acesse o contexto de execução completo com uma função seletora |
## API de comunicação do host
@@ -293,84 +286,14 @@ export default defineFrontComponent({
});
```
### Trabalhando com vários registros
## Opções de comando
Use `useSelectedRecordIds()` para lidar com vários registros selecionados. Isso é útil para operações em lote:
```tsx src/front-components/bulk-export.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds, numberOfSelectedRecords } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-sdk/clients';
const BulkExport = () => {
const selectedRecordIds = useSelectedRecordIds();
const handleExport = async () => {
const client = new CoreApiClient();
for (const recordId of selectedRecordIds) {
await client.mutation({
updateTask: {
__args: { id: recordId, data: { exported: true } },
id: true,
},
});
}
await enqueueSnackbar({
message: `Exported ${selectedRecordIds.length} records`,
variant: 'success',
});
await closeSidePanel();
};
return (
<div style={{ padding: '20px' }}>
<p>Export {selectedRecordIds.length} selected record(s)?</p>
<button onClick={handleExport}>Export</button>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
name: 'bulk-export',
description: 'Export selected records',
component: BulkExport,
command: {
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902',
label: 'Bulk Export',
availabilityType: 'RECORD_SELECTION',
conditionalAvailabilityExpression: numberOfSelectedRecords > 0,
},
});
```
## defineCommandMenuItem
Use `defineCommandMenuItem` para registrar um componente de front-end no menu de comando (Cmd+K). Se `isPinned` for `true`, ele também aparece como um botão de ação rápida no canto superior direito da página.
```ts src/command-menu-items/open-dashboard.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
label: 'Open Dashboard',
shortLabel: 'Dashboard',
icon: 'IconLayoutDashboard',
isPinned: true,
availabilityType: 'GLOBAL',
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
});
```
Adicionar um campo `command` a `defineFrontComponent` registra o componente no menu de comandos (Cmd+K). Se `isPinned` for `true`, ele também aparece como um botão de ação rápida no canto superior direito da página.
| Campo | Obrigatório | Descrição |
| --------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `universalIdentifier` | Sim | ID exclusivo e estável para o comando |
| `label` | Sim | Rótulo completo exibido no menu de comandos (Cmd+K) |
| `frontComponentUniversalIdentifier` | Sim | O `universalIdentifier` do componente de front-end que este comando abre |
| `shortLabel` | Não | Rótulo mais curto exibido no botão fixado de ação rápida |
| `icon` | Não | Nome do ícone exibido ao lado do rótulo (por exemplo, `'IconBolt'`, `'IconSend'`) |
| `isPinned` | Não | Quando `true`, mostra o comando como um botão de ação rápida no canto superior direito da página |
@@ -382,23 +305,30 @@ export default defineCommandMenuItem({
O campo `conditionalAvailabilityExpression` permite controlar quando um comando é visível com base no contexto da página atual. Importe variáveis tipadas e operadores de `twenty-sdk` para construir expressões:
```ts src/command-menu-items/bulk-update.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
```tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import {
pageType,
numberOfSelectedRecords,
objectPermissions,
everyEquals,
isDefined,
} from 'twenty-sdk/front-component';
export default defineCommandMenuItem({
export default defineFrontComponent({
universalIdentifier: '...',
label: 'Bulk Update',
availabilityType: 'RECORD_SELECTION',
frontComponentUniversalIdentifier: '...',
conditionalAvailabilityExpression: everyEquals(
objectPermissions,
'canUpdateObjectRecords',
true,
),
name: 'bulk-action',
component: BulkAction,
command: {
universalIdentifier: '...',
label: 'Bulk Update',
availabilityType: 'RECORD_SELECTION',
conditionalAvailabilityExpression: everyEquals(
objectPermissions,
'canUpdateObjectRecords',
true,
),
},
});
```
@@ -4,52 +4,48 @@ icon: rocket
description: Crie seu primeiro app do Twenty em minutos.
---
## O que são aplicativos?
Os aplicativos permitem que você estenda o Twenty com objetos e campos personalizados, funções lógicas, componentes de front-end, habilidades de IA e mais — tudo gerenciado como código. Em vez de configurar tudo pela UI, você define seu modelo de dados e a lógica em TypeScript e implanta em um ou mais workspaces.
## Pré-requisitos
* **Node.js 24+** — [Baixar](https://nodejs.org/)
* **Yarn 4** — Vem com o Node.js via Corepack. Ative-o: `corepack enable`
* **Docker** — [Baixar](https://www.docker.com/products/docker-desktop/). Necessário para executar um servidor Twenty local. Ignore se você já tiver o Twenty em execução em outro lugar.
Antes de começar, verifique se o seguinte está instalado na sua máquina:
A criação de um aplicativo Twenty tem três fases. A ferramenta de scaffolding as reúne em um único comando do fluxo ideal, mas cada fase é um conceito separado — quando algo falha, saber em que fase você está indica o que corrigir.
* **Node.js 24+** — [Baixe aqui](https://nodejs.org/)
* **Yarn 4** — Vem com o Node.js via Corepack. Ative-o executando `corepack enable`
* **Docker** — [Baixe aqui](https://www.docker.com/products/docker-desktop/). Necessário para executar uma instância local do Twenty. Não é necessário se você já tiver um servidor Twenty em execução.
| Fase | O que você faz | Ferramenta | Resultado |
| --------------------------- | -------------------------------------------------- | ----------------------------- | ------------------------------------- |
| **1. Criar scaffolding** | Gerar o código-fonte do aplicativo | `npx create-twenty-app` | Um projeto TypeScript em disco |
| **2. Executar um servidor** | Iniciar um servidor Twenty para o qual sincronizar | Docker + `yarn twenty server` | Uma instância Twenty em execução |
| **3. Sincronizar** | Sincronize seu código em tempo real com o servidor | `yarn twenty dev` | Suas alterações aparecem na interface |
## Crie seu primeiro aplicativo
---
### Gere o scaffold do seu aplicativo
## Fase 1 — Fazer scaffolding do seu projeto
Crie um novo aplicativo a partir do modelo:
Abra um terminal e execute:
```bash filename="Terminal"
npx create-twenty-app@latest my-twenty-app
```
Você será solicitado a informar um nome e uma descrição — pressione **Enter** para aceitar os valores padrão. Isso gera um projeto TypeScript em `my-twenty-app/` com um `application-config.ts` inicial, um papel padrão, um fluxo de trabalho de CI e um teste de integração.
Será solicitado que você informe um nome e uma descrição para o seu aplicativo. Pressione **Enter** para aceitar os valores padrão.
**Após esta fase:** você tem o código-fonte de um aplicativo na sua máquina. Ele ainda não está em execução — isso é a Fase 2.
Isso cria uma nova pasta chamada `my-twenty-app` com tudo de que você precisa.
---
### Configure uma instância local do Twenty
## Fase 2 — Executar um servidor Twenty local
Seu aplicativo precisa de um servidor Twenty para o qual sincronizar. O servidor é uma instância completa do Twenty — interface, API GraphQL, PostgreSQL — executando localmente no Docker. Seu código local envia suas definições para esse servidor, o que faz com que elas apareçam na interface.
A ferramenta de scaffolding oferece iniciar um para você:
O gerador de scaffold perguntará:
> **Você gostaria de configurar uma instância local do Twenty?**
* **Sim (recomendado)** — baixa a imagem Docker `twentycrm/twenty-app-dev` e a inicia na porta `2020`. Certifique-se de que o Docker esteja em execução primeiro.
* **Não** — escolha isto se você já tiver um servidor Twenty ao qual deseja se conectar. Você pode conectá-lo depois com `yarn twenty remote add`.
* **Digite `yes`** (recomendado) — Isso baixa a imagem Docker `twenty-app-dev` e inicia um servidor Twenty local na porta `2020`. Certifique-se de que o Docker esteja em execução antes de continuar.
* **Digite `no`** — Escolha esta opção se você já tiver um servidor Twenty em execução localmente.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/start-instance.png" alt="Deve iniciar instância local?" />
</div>
Quando o servidor estiver ativo, um navegador será aberto para login. Use a conta de demonstrão pré-configurada:
### Faça login no seu espaço de trabalho
Em seguida, uma janela do navegador será aberta com a página de login do Twenty. Faça login com a conta de demonstração pré-configurada:
* **E-mail:** `tim@apple.dev`
* **Senha:** `tim@apple.dev`
@@ -58,81 +54,83 @@ Quando o servidor estiver ativo, um navegador será aberto para login. Use a con
<img src="/images/docs/developers/extends/apps/login.png" alt="Tela de login do Twenty" />
</div>
Clique em **Authorize** na próxima tela — isso dá à CLI acesso ao seu espaço de trabalho.
### Autorize o aplicativo
Após fazer login, você verá uma tela de autorização. Isso permite que seu aplicativo interaja com seu espaço de trabalho.
Clique em **Authorize** para continuar.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/authorize.png" alt="Tela de autorização da CLI do Twenty" />
</div>
Seu terminal confirmará que tudo está configurado.
Depois de autorizado, seu terminal confirmará que tudo está configurado.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/scaffolded.png" alt="Scaffold do aplicativo criado com sucesso" />
</div>
**Após esta fase:** você tem um servidor Twenty em execução em [http://localhost:2020](http://localhost:2020) com sua CLI autorizada a sincronizar com ele.
### Comece a desenvolver
<Note>
Se o Docker não estiver instalado ou em execução, a ferramenta de scaffolding informará o comando de inicialização correto para o seu sistema operacional. Quando o Docker estiver ativo, você pode retomar com `yarn twenty server start` — sem necessidade de recriar o scaffolding.
</Note>
---
## Fase 3 — Sincronizar suas alterações
Este é o ciclo interno no qual você passará a maior parte do tempo.
Entre na nova pasta do seu aplicativo e inicie o servidor de desenvolvimento:
```bash filename="Terminal"
cd my-twenty-app
yarn twenty dev
```
Isso monitora `src/`, recompila a cada alteração e sincroniza o resultado com o servidor. Edite um arquivo, salve e, em um segundo, o servidor refletirá a alteração. Você verá um painel de status em tempo real no seu terminal.
Isso observa seus arquivos-fonte, recompila a cada alteração e sincroniza seu aplicativo com o servidor Twenty local automaticamente. Você deverá ver um painel de status em tempo real no seu terminal.
Para uma saída mais detalhada (logs de build, solicitações de sincronização, rastros de erro), adicione `--verbose`.
Para uma saída mais detalhada (logs de build, solicitações de sincronização, rastros de erro), use a flag `--verbose`:
```bash filename="Terminal"
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` 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.png" alt="Saída do terminal no modo de desenvolvimento" />
</div>
Abra [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer). Você deverá ver seu aplicativo em **Your Apps**.
#### Sincronização única com `yarn twenty dev --once`
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-1.png" alt="Lista Your Apps exibindo My twenty app" />
</div>
Clique em **My twenty app** para ver seu **registro do aplicativo** — um registro em nível de servidor que descreve seu aplicativo (nome, identificador, credenciais OAuth, origem). Um registro pode ser instalado em vários espaços de trabalho no mesmo servidor.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-2.png" alt="Detalhes do registro do aplicativo" />
</div>
Clique em **View installed app** para ver a instalação no espaço de trabalho. A aba **About** mostra a versão e as opções de gerenciamento.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Aplicação instalada" />
</div>
**Após esta fase:** você tem um ciclo de desenvolvimento em tempo real. Edite qualquer arquivo em `src/` e ele aparecerá na interface.
### Sincronização única para CI e scripts
Passe `--once` para executar uma única compilação + sincronização e sair — mesmo pipeline, sem watcher:
Se você não quiser um monitor em execução em segundo plano (por exemplo, em um pipeline de CI, um hook do git ou um fluxo de trabalho com script), passe a flag `--once`. Ele executa o mesmo pipeline que `yarn twenty dev` — gerar o manifesto, empacotar arquivos, fazer upload, sincronizar, regenerar o cliente de API tipado — mas **encerra assim que a sincronização for concluída**:
```bash filename="Terminal"
yarn twenty dev --once
```
| Comando | Comportamento | Quando usar |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| `yarn twenty dev` | Monitora e ressincroniza a cada alteração. Fica em execução até você interrompê-lo. | Desenvolvimento local interativo. |
| `yarn twenty dev --once` | Executa uma única compilação + sincronização e, em seguida, encerra com o código `0` em caso de sucesso ou `1` em caso de falha. | Scripts, CI, hooks de pre-commit, agentes de IA e fluxos de trabalho com script. |
| Comando | Comportamento | Quando usar |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `yarn twenty dev` | Monitora seus arquivos-fonte e ressincroniza a cada alteração. Continua em execução até você interrompê-lo. | Desenvolvimento local interativo — você quer o painel de status em tempo real e um ciclo de feedback instantâneo. |
| `yarn twenty dev --once` | Executa uma única compilação + sincronização e, em seguida, encerra com o código `0` em caso de sucesso ou `1` em caso de falha. | Scripts, CI, hooks de pre-commit, agentes de IA e qualquer fluxo de trabalho não interativo. |
Ambos os modos precisam de um servidor em modo de desenvolvimento e de um remoto autenticado.
Ambos os modos exigem um servidor Twenty em execução no modo de desenvolvimento e um remoto autenticado — aplicam-se os mesmos pré-requisitos.
<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 implantar em servidores de produção. Veja [Publicação de aplicativos](/l/pt/developers/extend/apps/publishing).
</Warning>
### Veja seu aplicativo no Twenty
Abra [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer) no seu navegador. Navegue até **Settings > Apps** e selecione a aba **Developer**. Você deverá ver seu aplicativo listado em **Your Apps**:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-1.png" alt="Lista Your Apps exibindo My twenty app" />
</div>
Clique em **My twenty app** para abrir o seu **registro do aplicativo**. Um registro é um registro em nível de servidor que descreve seu aplicativo — seu nome, identificador exclusivo, credenciais OAuth e origem (local, npm ou tarball). Ele reside no servidor, não dentro de nenhum espaço de trabalho específico. Quando você instala um aplicativo em um espaço de trabalho, o Twenty cria uma **aplicação** com escopo do espaço de trabalho que aponta para esse registro. Um registro pode ser instalado em vários espaços de trabalho no mesmo servidor.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-2.png" alt="Detalhes do registro do aplicativo" />
</div>
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="Aplicação instalada" />
</div>
Tudo pronto! Edite qualquer arquivo em `src/` e as alterações serão detectadas automaticamente.
---
@@ -142,110 +140,134 @@ Os aplicativos são compostos por **entidades** — cada uma definida como um ar
| Entidade | O que faz |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| **Objetos e campos** | Modelos de dados personalizados (Cartão postal, Fatura etc.) com campos tipados |
| **Objetos e campos** | Defina modelos de dados personalizados (como cartão postal, fatura) com campos tipados |
| **Funções lógicas** | Funções TypeScript do lado do servidor acionadas por rotas HTTP, agendamentos do cron ou eventos de banco de dados |
| **Componentes de front-end** | Componentes React que são renderizados na UI do Twenty (painel lateral, widgets, menu de comandos) |
| **Habilidades e agentes** | Recursos de IA — instruções reutilizáveis e assistentes autônomos |
| **Exibições e navegação** | Exibições de lista pré-configuradas e itens de menu da barra lateral |
| **Exibições e navegação** | Exibições de lista pré-configuradas e itens de menu da barra lateral para seus objetos |
| **Layouts de página** | Páginas de detalhes de registros personalizadas com abas e widgets |
Referência completa: [Criando aplicativos](/l/pt/developers/extend/apps/building).
Acesse [Criando aplicativos](/l/pt/developers/extend/apps/building) para um guia detalhado sobre cada tipo de entidade.
---
## Estrutura do projeto
A ferramenta de scaffolding gera a seguinte estrutura de arquivos:
```text filename="my-twenty-app/"
my-twenty-app/
package.json
yarn.lock
.gitignore
.nvmrc
.yarnrc.yml
.oxlintrc.json
tsconfig.json
tsconfig.spec.json # TypeScript config for tests
vitest.config.ts # Vitest test runner configuration
LLMS.md
README.md
.github/
└── workflows/
└── ci.yml # GitHub Actions CI workflow
public/ # Public assets (images, fonts, etc.)
src/
application-config.ts # Required — your app's entry point
default-role.ts # Permissions for logic functions
constants/
universal-identifiers.ts # Auto-generated UUIDs and metadata
__tests__/
setup-test.ts
app-install.integration-test.ts
.github/workflows/ci.yml # GitHub Actions
public/ # Static assets
vitest.config.ts # Test runner config
tsconfig.json, tsconfig.spec.json
.nvmrc, .yarnrc.yml, .oxlintrc.json
README.md, LLMS.md
├── application-config.ts # Required — main application configuration
├── default-role.ts # Default role for logic functions
├── constants/
└── universal-identifiers.ts # Auto-generated UUIDs and app metadata
└── __tests__/
├── setup-test.ts # Test setup (server health check, config)
└── app-install.integration-test.ts # Integration test
```
| Arquivo / Pasta | Finalidade |
| ---------------------------------------- | ------------------------------------------------------------------------ |
| `src/application-config.ts` | **Obrigatório.** O principal arquivo de configuração do seu aplicativo. |
| `src/default-role.ts` | Papel padrão que controla o que suas funções de lógica podem acessar. |
| `src/constants/universal-identifiers.ts` | UUIDs gerados automaticamente e metadados (nome de exibição, descrição). |
| `src/__tests__/` | Testes de integração (configuração + teste de exemplo). |
| `public/` | Recursos estáticos (imagens, fontes) servidos com seu aplicativo. |
### Começando a partir de um exemplo
Use `--example` para começar com um projeto mais completo (objetos personalizados, campos, funções de lógica, componentes de front-end):
Para começar a partir de um exemplo mais completo com objetos, campos, funções de lógica, componentes de front-end e mais, use a opção `--example`:
```bash filename="Terminal"
npx create-twenty-app@latest my-twenty-app --example postcard
```
Os exemplos estão em [twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples). Você também pode criar o scaffolding de entidades individuais em um projeto existente com `yarn twenty add` veja [Criando aplicativos](/l/pt/developers/extend/apps/building#scaffolding-entities-with-yarn-twenty-add).
Os exemplos são obtidos do diretório [twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples) no GitHub. Você também pode criar o scaffolding de entidades individuais em um projeto existente com `yarn twenty add` (veja [Criando aplicativos](/l/pt/developers/extend/apps/building#scaffolding-entities-with-yarn-twenty-add)).
---
### Arquivos principais
## Gerenciando o servidor local
| Arquivo / Pasta | Finalidade |
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `package.json` | Declara o nome, a versão e as dependências do seu aplicativo. Inclui um script `twenty` para que você possa executar `yarn twenty help` e ver todos os comandos. |
| `src/application-config.ts` | **Obrigatório.** O principal arquivo de configuração do seu aplicativo. |
| `src/default-role.ts` | Papel padrão que controla o que suas funções de lógica podem acessar. |
| `src/constants/universal-identifiers.ts` | UUIDs gerados automaticamente e metadados do aplicativo (nome de exibição, descrição). |
| `src/__tests__/` | Testes de integração (configuração + teste de exemplo). |
| `public/` | Recursos estáticos (imagens, fontes) servidos com seu aplicativo. |
Use `yarn twenty server` para controlar o contêiner Twenty local:
## Servidor de desenvolvimento local
| Comando | O que faz |
| -------------------------------------- | ------------------------------------------------ |
| `yarn twenty server start` | Inicia o servidor (baixa a imagem se necessário) |
| `yarn twenty server start --port 3030` | Iniciar em uma porta personalizada |
| `yarn twenty server stop` | Interrompe o servidor (preserva os dados) |
| `yarn twenty server status` | Mostra a URL, a versão e as credenciais de login |
| `yarn twenty server logs` | Transmite os logs do servidor |
| `yarn twenty server reset` | Apaga os dados e começa do zero |
| `yarn twenty server upgrade` | Baixa a imagem mais recente `twenty-app-dev` |
| `yarn twenty server upgrade 2.2.0` | Atualizar para uma versão específica |
A ferramenta de scaffolding já iniciou um servidor local do Twenty para você. Para gerenciá-lo depois, use `yarn twenty server`:
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.
| 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
`yarn twenty server upgrade` baixa a imagem mais recente, compara os digests e só recria o contêiner se algo realmente tiver mudado. Os volumes são preservados — apenas o contêiner é substituído. Se uma nova imagem foi baixada e o contêiner estava em execução, a atualização inicia automaticamente um novo contêiner; execute `yarn twenty server start` depois para aguardar até que ele fique saudável.
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"
yarn twenty server upgrade # Latest
yarn twenty server upgrade 2.2.0 # Specific version
# 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
```
Verifique a versão em execução com `yarn twenty server status` (ele mostra o `APP_VERSION` incorporado ao contêiner).
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.
### Executando uma instância de teste paralela
Você pode verificar a versão em execução com `yarn twenty server status`, que exibe o `APP_VERSION` do container.
Passe `--test` para qualquer comando de `server` para gerenciar uma segunda instância totalmente isolada — útil para testes de integração ou para experimentar sem tocar nos seus dados principais de desenvolvimento:
### Executando uma instância de teste
| Comando | O que faz |
| ----------------------------------- | ------------------------------------------------ |
| `yarn twenty server start --test` | Inicia a instância de teste (padrão: porta 2021) |
| `yarn twenty server stop --test` | Parar |
| `yarn twenty server status --test` | Mostrar seu status |
| `yarn twenty server logs --test` | Transmitir seus logs |
| `yarn twenty server reset --test` | Apagar seus dados |
| `yarn twenty server upgrade --test` | Atualizar sua imagem |
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.
A instância de teste tem seu próprio contêiner (`twenty-app-dev-test`), volumes (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`) e configuração — ela é executada junto com sua instância principal sem conflitos. Combine `--test` com `--port` para substituir 2021.
| 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.
<Note>
O servidor requer que o **Docker** esteja em execução. Se você vir um erro "Docker not running", certifique-se de que o Docker Desktop (ou o daemon do Docker) esteja iniciado.
</Note>
## Configuração manual (sem o gerador)
Ignore a ferramenta de scaffolding se você estiver adicionando o SDK a um projeto existente:
Se preferir configurar tudo por conta própria em vez de usar `create-twenty-app`, você pode fazer isso em duas etapas.
**1. Adicione `twenty-sdk` e `twenty-client-sdk` como dependências:**
```bash filename="Terminal"
yarn add twenty-sdk twenty-client-sdk
```
Adicione o script ao `package.json`:
**2. Adicione um script `twenty` ao seu `package.json`:**
```json filename="package.json"
{
@@ -255,19 +277,19 @@ Adicione o script ao `package.json`:
}
```
Agora você pode executar `yarn twenty dev`, `yarn twenty server start` e o restante.
Agora você pode executar `yarn twenty dev`, `yarn twenty help` e todos os outros comandos.
<Note>
Não instale `twenty-sdk` globalmente — fixe-o por projeto, para que cada aplicativo use sua própria versão.
Não instale o `twenty-sdk` globalmente. Use-o sempre como uma dependência local do projeto para que cada projeto possa fixar sua própria versão.
</Note>
---
## Resolução de Problemas
* **Erros do Docker** — Certifique-se de que o Docker Desktop (ou o daemon) esteja em execução antes de `yarn twenty server start`. A mensagem de erro mostrará o comando de inicialização correto para o seu sistema operacional.
* **Versão errada do Node** — É necessário 24 ou superior. Verifique com `node -v`.
* **Falta o Yarn 4** — Execute `corepack enable`.
* **Dependências com problemas** — `rm -rf node_modules && yarn install`.
Se você tiver problemas:
Travou? Peça ajuda no [Discord da Twenty](https://discord.com/channels/1130383047699738754/1130386664812982322).
* Certifique-se de que o **Docker está em execução** antes de iniciar o scaffolder com uma instância local.
* Certifique-se de que está usando **Node.js 24+** (`node -v` para verificar).
* Certifique-se de que o **Corepack está ativado** (`corepack enable`) para que o Yarn 4 esteja disponível.
* Tente excluir `node_modules` e executar `yarn install` novamente se as dependências parecerem corrompidas.
Ainda com dificuldades? Peça ajuda no [Discord da Twenty](https://discord.com/channels/1130383047699738754/1130386664812982322).
@@ -141,14 +141,11 @@ const handler = async (event: RoutePayload) => {
Os nomes dos cabeçalhos são normalizados para minúsculas. Acesse-os usando chaves em minúsculas (por exemplo, `event.headers['content-type']`).
</Note>
#### Expor uma função como ferramenta de IA ou como ação de fluxo de trabalho
#### Expor uma função como ferramenta
As funções de lógica podem ser expostas em duas superfícies, cada uma com seu próprio gatilho:
Funções lógicas podem ser expostas como **ferramentas** para agentes de IA e fluxos de trabalho. Quando marcada como ferramenta, uma função fica detectável pelos recursos de IA do Twenty e pode ser usada em automações de fluxos de trabalho.
* **`toolTriggerSettings`** — torna a função disponível para os recursos de IA do Twenty (chat, MCP, chamadas de função). Usa o JSON Schema padrão, o formato que os LLMs entendem nativamente.
* **`workflowActionTriggerSettings`** — torna a função visível como uma etapa no construtor visual de fluxos de trabalho. Usa o `InputSchema` avançado do Twenty para que o construtor possa renderizar editores de campo adequados, seletores de variáveis e rótulos.
Uma função pode optar por uma, pela outra ou por ambas. Ficam ao lado de `cronTriggerSettings`, `databaseEventTriggerSettings` e `httpRouteTriggerSettings` — mesmo padrão, mesmo formato.
Para marcar uma função de lógica como ferramenta, defina `isTool: true`:
```ts src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
@@ -178,33 +175,31 @@ export default defineLogicFunction({
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
toolTriggerSettings: {},
isTool: true,
});
```
Pontos-chave:
* Uma função pode misturar superfícies — declare tanto `toolTriggerSettings` quanto `workflowActionTriggerSettings` para expô-la no chat E no construtor de fluxos de trabalho.
* `toolTriggerSettings.inputSchema` e `workflowActionTriggerSettings.inputSchema` são opcionais. Quando omitidos, o construtor de manifestos os infere a partir do código-fonte do handler (JSON Schema para a ferramenta de IA, `InputSchema` do Twenty para a ação de fluxo de trabalho). Forneça um explicitamente quando quiser uma tipagem mais rica — por exemplo, com campos compatíveis com `FieldMetadataType`, como `CURRENCY` ou `RELATION` para o construtor de fluxos de trabalho, ou com campos `description` que o agente de IA pode ler:
* Você pode combinar `isTool` com gatilhos — uma função pode ser ao mesmo tempo uma ferramenta (chamável por agentes de IA) e acionada por eventos.
* **`toolInputSchema`** (opcional): Um objeto JSON Schema que descreve os parâmetros que sua função aceita. O schema é calculado automaticamente a partir da análise estática do código-fonte, mas você pode defini-lo explicitamente:
```ts
export default defineLogicFunction({
...,
toolTriggerSettings: {
inputSchema: {
type: 'object',
properties: {
companyName: {
type: 'string',
description: 'The name of the company to enrich',
},
domain: {
type: 'string',
description: 'The company website domain (optional)',
},
toolInputSchema: {
type: 'object',
properties: {
companyName: {
type: 'string',
description: 'The name of the company to enrich',
},
domain: {
type: 'string',
description: 'The company website domain (optional)',
},
required: ['companyName'],
},
required: ['companyName'],
},
});
```
@@ -243,7 +238,7 @@ yarn twenty exec --postInstall
```
Pontos-chave:
* As funções de pós-instalação usam `definePostInstallLogicFunction()` — uma variante especializada que omite as configurações de gatilho (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `toolTriggerSettings`, `workflowActionTriggerSettings`).
* As funções de pós-instalação usam `definePostInstallLogicFunction()` — uma variante especializada que omite as configurações de gatilho (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
* O manipulador recebe um `InstallPayload` com `{ previousVersion?: string; newVersion: string }` — `newVersion` é a versão que está sendo instalada, e `previousVersion` é a versão que foi instalada anteriormente (ou `undefined` em uma instalação nova). Use esses valores para distinguir instalações novas de atualizações e para executar lógica de migração específica da versão.
* **Quando o hook é executado**: apenas em instalações novas, por padrão. Passe `shouldRunOnVersionUpgrade: true` se você também quiser que ele seja executado quando o app for atualizado a partir de uma versão anterior. Quando omitida, a flag tem valor padrão `false` e as atualizações ignoram o hook.
* **Modelo de execução — assíncrono por padrão, síncrono opcional**: a flag `shouldRunSynchronously` controla *como* a pós-instalação é executada.
@@ -198,14 +198,6 @@ export default defineApplication({
Veja o [acordeão de defineApplication](/l/pt/developers/extend/apps/building#defineentity-functions) na página Building Apps para a lista completa de campos do marketplace (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl`, etc.).
#### Dimensões recomendadas para capturas de tela
O marketplace renderiza `screenshots` em um contêiner fixo de `8:5` (por exemplo, `1600×1000 px`).
<Note>
Capturas de tela de qualquer proporção são exibidas por completo e nunca são cortadas, mas qualquer coisa significativamente mais alta ou mais estreita que `8:5` exibirá faixas vazias nas laterais.
</Note>
### Publicar
```bash filename="Terminal"
@@ -225,9 +217,9 @@ O servidor Twenty sincroniza seu catálogo do marketplace a partir do registro d
Você pode acionar a sincronização imediatamente em vez de esperar:
```bash filename="Terminal"
yarn twenty server catalog-sync
yarn twenty catalog-sync
# To target a specific remote:
# yarn twenty server catalog-sync --remote production
# yarn twenty catalog-sync --remote production
```
Os metadados exibidos no marketplace vêm da sua configuração `defineApplication()` — campos como `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl` e `termsUrl`.
@@ -1,6 +1,5 @@
---
title: Comenzi Backend
icon: terminal
---
## Comenzi utile
@@ -1,6 +1,5 @@
---
title: Bug-uri, solicitări și pull request-uri
icon: bug
info: Raportați probleme, solicitați funcționalități și contribuiți cu cod
---
@@ -1,6 +1,5 @@
---
title: Cele mai bune practici
icon: star
---
Acest document prezintă cele mai bune practici pe care ar trebui să le urmați atunci când lucrați la frontend.
@@ -1,6 +1,5 @@
---
title: Arhitectura Folderului
icon: folder-tree
info: O privire detaliată asupra arhitecturii folderului nostru
---
@@ -1,6 +1,5 @@
---
title: Comenzi Frontend
icon: terminal
---
## Comenzi utile
@@ -1,6 +1,5 @@
---
title: Ghid de stil
icon: paintbrush
---
Acest document include regulile ce trebuie urmate la scrierea codului.
@@ -1,6 +1,5 @@
---
title: Configurare locală
icon: laptop-code
description: Ghidul pentru contribuitori (sau dezvoltatori curioși) care doresc să ruleze Twenty local.
---
@@ -1,77 +0,0 @@
---
title: Comenzi
icon: terminal
description: Comenzi utile pentru dezvoltarea Twenty.
---
Comenzile pot fi rulate din rădăcina repozitoriului folosind `npx nx`. Folosiți `npx nx run {project}:{command}` pentru a specifica în mod explicit ținta.
## Pornirea aplicației
```bash
npx nx start twenty-front # Frontend dev server (http://localhost:3001)
npx nx start twenty-server # Backend server (http://localhost:3000)
npx nx run twenty-server:worker # Background worker
```
## Bază de date
```bash
npx nx database:reset twenty-server # Reset and seed database
npx nx run twenty-server:database:migrate:prod # Run migrations
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow> # Generate a migration
```
## Linting
```bash
npx nx lint:diff-with-main twenty-front # Lint changed files (fastest)
npx nx lint:diff-with-main twenty-server
npx nx lint twenty-front --configuration=fix # Auto-fix
```
## Verificarea tipurilor
```bash
npx nx typecheck twenty-front
npx nx typecheck twenty-server
```
## Testare
```bash
# Frontend
npx nx test twenty-front # Jest unit tests
npx nx storybook:build twenty-front # Build Storybook
npx nx storybook:test twenty-front # Storybook tests
# Backend
npx nx run twenty-server:test:unit # Unit tests
npx nx run twenty-server:test:integration # Integration tests
npx nx run twenty-server:test:integration:with-db-reset # Integration with DB reset
# Single file (fastest)
npx jest path/to/test.test.ts --config=packages/{project}/jest.config.mjs
```
## GraphQL
```bash
npx nx run twenty-front:graphql:generate # Regenerate types
npx nx run twenty-front:graphql:generate --configuration=metadata # Metadata schema
```
## Traduceri
```bash
npx nx run twenty-front:lingui:extract # Extract strings
npx nx run twenty-front:lingui:compile # Compile translations
```
## Build
```bash
npx nx build twenty-shared # Must be built first
npx nx build twenty-front
npx nx build twenty-server
```
@@ -1,176 +0,0 @@
---
title: Ghid de stil
icon: paintbrush
description: Convenții de cod și bune practici pentru a contribui la Twenty.
---
## React
### Doar componente funcționale
Folosește întotdeauna componente funcționale TSX cu exporturi denumite.
```tsx
// ❌ Bad
const MyComponent = () => {
return <div>Hello World</div>;
};
export default MyComponent;
// ✅ Good
export function MyComponent() {
return <div>Hello World</div>;
};
```
### Proprietăți
Creează un tip numit `{ComponentName}Props`. Folosește destructurarea. Nu folosi `React.FC`.
```tsx
type MyComponentProps = {
name: string;
};
export const MyComponent = ({ name }: MyComponentProps) => <div>Hello {name}</div>;
```
### Fără spread al unei singure variabile de props
```tsx
// ❌ Bad
const MyComponent = (props: MyComponentProps) => <Other {...props} />;
// ✅ Good
const MyComponent = ({ prop1, prop2 }: MyComponentProps) => <Other {...{ prop1, prop2 }} />;
```
## Managementul stării
### Atomi Jotai pentru starea globală
```tsx
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
export const myAtomState = createAtomState<string>({
key: 'myAtomState',
defaultValue: 'default value',
});
```
* Preferă atomii în locul prop drilling-ului
* Nu folosi `useRef` pentru stare — folosește `useState` sau atomi
* Folosește familii de atomi și selectori pentru liste
### Evită re-randări inutile
* Extrage `useEffect` și preluarea datelor în componente surori de tip sidecar
* Preferă handleri de evenimente (`handleClick`, `handleChange`) în locul lui `useEffect`
* Nu folosi `React.memo()` — în schimb, rezolvă cauza principală
* Limitează utilizarea `useCallback` / `useMemo`
```tsx
// ❌ Bad — useEffect in the same component causes re-renders
export const Page = () => {
const [data, setData] = useAtomState(dataState);
const [dep] = useAtomState(depState);
useEffect(() => { setData(dep); }, [dep]);
return <div>{data}</div>;
};
// ✅ Good — extract into sibling
export const PageData = () => {
const [data, setData] = useAtomState(dataState);
const [dep] = useAtomState(depState);
useEffect(() => { setData(dep); }, [dep]);
return <></>;
};
export const Page = () => {
const [data] = useAtomState(dataState);
return <div>{data}</div>;
};
```
## TypeScript
* **`type` în loc de `interface`** — mai flexibil, mai ușor de compus
* **Șiruri literale în loc de enum-uri** — cu excepția enum-urilor generate de GraphQL codegen și a API-urilor interne ale bibliotecii
* **Fără `any`** — TypeScript strict este impus
* **Fără importuri de tip** — folosește importuri obișnuite (impus de Oxlint `typescript/consistent-type-imports`)
* **Folosește [Zod](https://github.com/colinhacks/zod)** pentru validarea în timp de execuție a obiectelor netipizate
## JavaScript
```tsx
// Use nullish-coalescing (??) instead of ||
const value = process.env.MY_VALUE ?? 'default';
// Use optional chaining
onClick?.();
```
## Denumiri
* **Variabile**: camelCase, descriptive (`email` nu `value`, `fieldMetadata` nu `fm`)
* **Constante**: SCREAMING_SNAKE_CASE
* **Tipuri/Clase**: PascalCase
* **Fișiere/directoare**: kebab-case (`.component.tsx`, `.service.ts`, `.entity.ts`)
* **Handleri de evenimente**: `handleClick` (nu `onClick` pentru funcția handler)
* **Props de componentă**: prefixează cu numele componentei (`ButtonProps`)
* **Componente stilizate**: prefixează cu `Styled` (`StyledTitle`)
## Stilizare
Folosește componente stilizate [Linaria](https://github.com/callstack/linaria). Folosește valorile din temă — evită `px`, `rem` sau culori hardcodate.
```tsx
// ❌ Bad
const StyledButton = styled.button`
color: #333333;
font-size: 1rem;
margin-left: 4px;
`;
// ✅ Good
const StyledButton = styled.button`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.md};
margin-left: ${({ theme }) => theme.spacing(1)};
`;
```
## Importuri
Folosește aliasuri în locul căilor relative:
```tsx
// ❌ Bad
import { Foo } from '../../../../../testing/decorators/Foo';
// ✅ Good
import { Foo } from '~/testing/decorators/Foo';
import { Bar } from '@/modules/bar/components/Bar';
```
## Structura folderelor
```
front
└── modules/ # Feature modules
│ └── module1/
│ ├── components/
│ ├── constants/
│ ├── contexts/
│ ├── graphql/ (fragments, queries, mutations)
│ ├── hooks/
│ ├── states/ (atoms, selectors)
│ ├── types/
│ └── utils/
└── pages/ # Route-level components
└── ui/ # Reusable UI components (display, input, feedback, ...)
```
* Modulele pot importa din alte module, dar `ui/` ar trebui să rămână fără dependențe
* Folosește subfoldere `internal/` pentru cod privat modulului
* Componente sub 300 de linii, servicii sub 500 de linii
@@ -1,55 +1,147 @@
---
title: API-uri
icon: plug
description: API-urile REST și GraphQL generate din schema spațiului tău de lucru.
description: Interogați și modificați programatic datele din CRM folosind REST sau GraphQL.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
## API-uri cu schemă per-tenant
Twenty a fost creat pentru a fi prietenos cu dezvoltatorii, oferind API-uri puternice care se adaptează la modelul dvs. de date personalizat. Oferim patru tipuri distincte de API-uri pentru a satisface diferite nevoi de integrare.
Nu există o referință API statică pentru Twenty. Fiecare spațiu de lucru are propria schemă — când adaugi un obiect personalizat (de exemplu `Invoice`), acesta primește imediat endpoint-uri REST și GraphQL identice cu cele ale obiectelor încorporate precum `Company` sau `Person`. API-ul este generat din schemă, astfel încât endpoint-urile folosesc direct denumirile obiectelor și câmpurilor tale — fără ID-uri opace.
## Abordare orientată către dezvoltatori
Documentația API specifică spațiului tău de lucru este disponibilă la **Settings → API & Webhooks** după crearea unei chei API. Include un mediu interactiv de testare în care poți executa apeluri reale asupra datelor tale.
Twenty generează API-uri special pentru modelul dvs. de date:
## Două API-uri
* **Nu sunt necesare ID-uri lungi**: Utilizați direct numele obiectelor și câmpurilor în punctele finale
* **Obiectele standard și personalizate tratate în mod egal**: Obiectele dvs. personalizate primesc același tratament API ca și cele încorporate
* **Puncte finale dedicate**: Fiecare obiect și câmp primește propriul său punct final API
* **Documentație personalizată**: Generată special pentru modelul de date al spațiului dvs. de lucru
**API de bază** — `/rest/` și `/graphql/`
<Note>
Documentația API personalizată este disponibilă la **Settings → API & Webhooks** după crearea unei chei API. Deoarece Twenty generează API-uri care se potrivesc modelului dvs. de date personalizat, documentația este unică pentru spațiul dvs. de lucru.
</Note>
CRUD pe înregistrări: Persoane, Companii, Oportunități, obiectele tale personalizate. Interogare, filtrare, parcurgere a relațiilor.
## Cele două tipuri de API-uri
**API de metadate** — `/rest/metadata/` și `/metadata/`
### API Core
Administrarea schemei: creare/modificare/ștergere de obiecte, câmpuri și relații. Așa îți modifici programatic modelul de date.
Accesibil prin `/rest/` sau `/graphql/`
Ambele sunt disponibile ca REST și GraphQL. GraphQL adaugă upsert-uri în lot și posibilitatea de a parcurge relațiile într-o singură interogare. Aceleași date de bază, indiferent de metodă.
Lucrați cu **înregistrările** reale (datele):
## URL-uri de bază
* Creați, citiți, actualizați, ștergeți Persoane, Companii, Oportunități etc.
* Interogați și filtrați datele
* Gestionați relațiile dintre înregistrări
| Mediu | URL de bază |
| ---------------- | ------------------------- |
| Cloud | `https://api.twenty.com/` |
| Găzduire proprie | `https://{your-domain}/` |
### API Metadata
Accesibil prin `/rest/metadata/` sau `/metadata/`
Gestionați-vă **spațiul de lucru și modelul de date**:
* Creați, modificați sau ștergeți obiecte și câmpuri
* Configurați setările spațiului de lucru
* Definiți relațiile dintre obiecte
## REST vs GraphQL
Atât API-urile Core, cât și API-urile Metadata sunt disponibile în formatele REST și GraphQL:
| Format | Operațiuni disponibile |
| ----------- | -------------------------------------------------------------------------- |
| **REST** | CRUD, operațiuni de grup, upsert-uri |
| **GraphQL** | La fel + **upsert-uri de grup**, interogări de relații într-un singur apel |
Alegeți în funcție de nevoi — ambele formate accesează aceleași date.
## Puncte Finale API
| Mediu | URL de bază |
| -------------------- | ------------------------- |
| **Cloud** | `https://api.twenty.com/` |
| **Găzduire proprie** | `https://{your-domain}/` |
## Autentificare
Fiecare solicitare API necesită o cheie API în antet:
```
Authorization: Bearer YOUR_API_KEY
```
Creează o cheie API în **Settings → API & Webhooks → + Create key**. Copiază-o imediat — este afișată o singură dată. Cheile pot fi limitate la un rol specific în **Settings → Roles → Assignment tab** pentru a restricționa la ce pot avea acces.
### Creați o cheie API
1. Mergeți la **Setări → API-uri & Webhook-uri**
2. Faceți clic pe **+ Create key**
3. Configurați:
* **Name**: Nume descriptiv pentru cheie
* **Expiration Date**: Când expiră cheia
4. Faceți clic pe **Salvare**
5. **Copiați imediat** — cheia este afișată o singură dată
<VimeoEmbed videoId="928786722" title="Crearea unei chei API" />
Pentru acces bazat pe OAuth (aplicații externe care acționează în numele utilizatorilor), vezi [OAuth](/l/ro/developers/extend/oauth).
<Warning>
Cheia dvs. API oferă acces la date sensibile. Nu o partajați cu servicii care nu sunt de încredere. Dacă este compromisă, dezactivați-o imediat și generați una nouă.
</Warning>
### Atribuiți un rol unei chei API
Pentru o securitate sporită, atribuiți un rol specific pentru a limita accesul:
1. Accesați **Setări → Roluri**
2. Faceți clic pe rolul pe care doriți să-l atribuiți
3. Deschideți fila **Atribuire**
4. În **API Keys**, faceți clic pe **+ Assign to API key**
5. Selectați cheia API
Cheia va moșteni permisiunile acelui rol. Consultați [Permisiuni](/l/ro/user-guide/permissions-access/capabilities/permissions) pentru detalii.
### Gestionați cheile API
**Regenerate**: Settings → APIs & Webhooks → Faceți clic pe cheie → **Regenerate**
**Delete**: Settings → APIs & Webhooks → Faceți clic pe cheie → **Delete**
## Platformă de testare API
Testați API-urile direct în browser cu platforma noastră integrată de testare — disponibilă atât pentru **REST**, cât și pentru **GraphQL**.
### Accesați platforma de testare
1. Mergeți la **Setări → API-uri & Webhook-uri**
2. Creați o cheie API (obligatoriu)
3. Faceți clic pe **REST API** sau **GraphQL API** pentru a deschide platforma de testare
### Ce obțineți
* **Documentație interactivă**: Generată pentru modelul dvs. de date specific
* **Testare live**: Executați apeluri API reale către spațiul dvs. de lucru
* **Explorator de scheme**: Parcurgeți obiectele, câmpurile și relațiile disponibile
* **Constructor de cereri**: Construiți interogări cu completare automată
Platforma de testare reflectă obiectele și câmpurile dvs. personalizate, astfel încât documentația este întotdeauna corectă pentru spațiul dvs. de lucru.
## Operațiuni de grup
Atât REST, cât și GraphQL acceptă procesarea în lot de până la 60 de înregistrări per cerere — creare, actualizare sau ștergere. GraphQL acceptă, de asemenea, upsert în lot (creare-sau-actualizare într-un singur apel) folosind nume la plural precum `CreateCompanies`.
Atât REST, cât și GraphQL suportă operațiuni de grup:
* **Dimensiunea grupului**: Până la 60 de înregistrări pe cerere
* **Operațiuni**: Creați, actualizați, ștergeți mai multe înregistrări
**Funcții exclusive GraphQL:**
* **Upsert de grup**: Creați sau actualizați într-un singur apel
* Folosiți nume de obiecte la plural (de exemplu, `CreateCompanies` în loc de `CreateCompany`)
## Limitări de rată
| Limită | Valoare |
| ------------------- | -------------------------- |
| Solicitări | 100 pe minut |
| Dimensiunea lotului | 60 de înregistrări pe apel |
Solicitările API sunt limitate pentru a asigura stabilitatea platformei:
| Limită | Valoare |
| ----------------------- | -------------------------- |
| **Solicitări** | 100 de apeluri pe minut |
| **Dimensiunea lotului** | 60 de înregistrări pe apel |
<Tip>
Utilizați operațiunile de grup pentru a maximiza debitul — procesați până la 60 de înregistrări într-un singur apel API în loc să faceți solicitări individuale.
</Tip>
File diff suppressed because it is too large Load Diff
@@ -1,434 +0,0 @@
---
title: CLI & Testare
description: Comenzi CLI, configurare pentru testare, resurse publice, pachete npm, remote-uri și configurare CI.
icon: terminal
---
## Resurse publice (folderul `public/`)
Folderul `public/` din rădăcina aplicației conține fișiere statice — imagini, pictograme, fonturi sau orice alte resurse de care are nevoie aplicația la rulare. Aceste fișiere sunt incluse automat în build-uri, sincronizate în timpul modului de dezvoltare și încărcate pe server.
Fișierele plasate în `public/` sunt:
* **Accesibile public** — odată sincronizate pe server, resursele sunt servite la un URL public. Nu este necesară autentificarea pentru a le accesa.
* **Disponibile în componentele frontend** — folosiți URL-urile resurselor pentru a afișa imagini, pictograme sau orice media în componentele React.
* **Disponibile în funcțiile logice** — referiți URL-urile resurselor în e-mailuri, răspunsuri API sau orice logică pe server.
* **Utilizate pentru metadatele marketplace-ului** — câmpurile `logoUrl` și `screenshots` din `defineApplication()` fac referire la fișiere din acest folder (de ex., `public/logo.png`). Acestea sunt afișate în marketplace când aplicația este publicată.
* **Sincronizate automat în modul de dezvoltare** — când adăugați, actualizați sau ștergeți un fișier în `public/`, acesta este sincronizat automat cu serverul. Nu este nevoie de repornire.
* **Incluse în build-uri** — `yarn twenty build` împachetează toate resursele publice în outputul de distribuție.
### Accesarea resurselor publice cu `getPublicAssetUrl`
Utilizați helperul `getPublicAssetUrl` din `twenty-sdk` pentru a obține URL-ul complet al unui fișier din directorul `public/`. Funcționează atât în funcții logice, cât și în componente frontend.
**Într-o funcție logică:**
```ts src/logic-functions/send-invoice.ts
import { defineLogicFunction, getPublicAssetUrl } from 'twenty-sdk/define';
const handler = async (): Promise<any> => {
const logoUrl = getPublicAssetUrl('logo.png');
const invoiceUrl = getPublicAssetUrl('templates/invoice.png');
// Fetch the file content (no auth required — public endpoint)
const response = await fetch(invoiceUrl);
const buffer = await response.arrayBuffer();
return { logoUrl, size: buffer.byteLength };
};
export default defineLogicFunction({
universalIdentifier: 'a1b2c3d4-...',
name: 'send-invoice',
description: 'Sends an invoice with the app logo',
timeoutSeconds: 10,
handler,
});
```
**Într-o componentă frontend:**
```tsx src/front-components/company-card.tsx
import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define';
export default defineFrontComponent(() => {
const logoUrl = getPublicAssetUrl('logo.png');
return <img src={logoUrl} alt="App logo" />;
});
```
Argumentul `path` este relativ la folderul `public/` al aplicației. Atât `getPublicAssetUrl('logo.png')`, cât și `getPublicAssetUrl('public/logo.png')` se rezolvă la același URL — prefixul `public/` este eliminat automat dacă este prezent.
## Utilizarea pachetelor npm
Puteți instala și utiliza orice pachet npm în aplicația dvs. Atât funcțiile logice, cât și componentele frontend sunt împachetate cu [esbuild](https://esbuild.github.io/), care integrează toate dependențele în output — nu sunt necesare `node_modules` la rulare.
### Instalarea unui pachet
```bash filename="Terminal"
yarn add axios
```
Apoi importați-l în codul dvs.:
```ts src/logic-functions/fetch-data.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import axios from 'axios';
const handler = async (): Promise<any> => {
const { data } = await axios.get('https://api.example.com/data');
return { data };
};
export default defineLogicFunction({
universalIdentifier: '...',
name: 'fetch-data',
description: 'Fetches data from an external API',
timeoutSeconds: 10,
handler,
});
```
Același lucru funcționează și pentru componentele frontend:
```tsx src/front-components/chart.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { format } from 'date-fns';
const DateWidget = () => {
return <p>Today is {format(new Date(), 'MMMM do, yyyy')}</p>;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'date-widget',
component: DateWidget,
});
```
### Cum funcționează împachetarea
Pasul de build folosește esbuild pentru a produce un singur fișier autonom pentru fiecare funcție logică și pentru fiecare componentă frontend. Toate pachetele importate sunt integrate în bundle.
**Funcțiile logice** rulează într-un mediu Node.js. Modulele built-in Node (`fs`, `path`, `crypto`, `http` etc.) sunt disponibile și nu trebuie instalate.
**Componentele frontend** rulează într-un Web Worker. Modulele built-in Node nu sunt disponibile — doar API-urile de browser și pachetele npm care funcționează într-un mediu de browser.
Ambele medii au `twenty-client-sdk/core` și `twenty-client-sdk/metadata` disponibile ca module pre-furnizate — acestea nu sunt incluse în bundle, ci sunt rezolvate la rulare de către server.
## Testarea aplicației
SDK-ul oferă API-uri programatice care vă permit să construiți, să distribuiți, să instalați și să dezinstalați aplicația din codul de test. Combinat cu [Vitest](https://vitest.dev/) și clienții API tipizați, puteți scrie teste de integrare care verifică faptul că aplicația funcționează cap-coadă împotriva unui server Twenty real.
### Configurare
Aplicația generată (scaffolded) include deja Vitest. Dacă o configurați manual, instalați dependențele:
```bash filename="Terminal"
yarn add -D vitest vite-tsconfig-paths
```
Creați un `vitest.config.ts` în rădăcina aplicației:
```ts vitest.config.ts
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [
tsconfigPaths({
projects: ['tsconfig.spec.json'],
ignoreConfigErrors: true,
}),
],
test: {
testTimeout: 120_000,
hookTimeout: 120_000,
include: ['src/**/*.integration-test.ts'],
setupFiles: ['src/__tests__/setup-test.ts'],
env: {
TWENTY_API_URL: 'http://localhost:2020',
TWENTY_API_KEY: 'your-api-key',
},
},
});
```
Creați un fișier de configurare care verifică faptul că serverul este accesibil înainte de rularea testelor:
```ts src/__tests__/setup-test.ts
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { beforeAll } from 'vitest';
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
beforeAll(async () => {
// Verify the server is running
const response = await fetch(`${TWENTY_API_URL}/healthz`);
if (!response.ok) {
throw new Error(
`Twenty server is not reachable at ${TWENTY_API_URL}. ` +
'Start the server before running integration tests.',
);
}
// Write a temporary config for the SDK
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
fs.writeFileSync(
path.join(TEST_CONFIG_DIR, 'config.json'),
JSON.stringify({
remotes: {
local: {
apiUrl: process.env.TWENTY_API_URL,
apiKey: process.env.TWENTY_API_KEY,
},
},
defaultRemote: 'local',
}, null, 2),
);
});
```
### API-uri SDK programatice
Subruta `twenty-sdk/cli` exportă funcții pe care le puteți apela direct din codul de test:
| Funcție | Descriere |
| -------------- | --------------------------------------------------------- |
| `appBuild` | Construiți aplicația și, opțional, împachetați un tarball |
| `appDeploy` | Încărcați un tarball pe server |
| `appInstall` | Instalați aplicația în spațiul de lucru activ |
| `appUninstall` | Dezinstalați aplicația din spațiul de lucru activ |
Fiecare funcție returnează un obiect rezultat cu `success: boolean` și fie `data`, fie `error`.
### Scrierea unui test de integrare
Iată un exemplu complet care construiește, distribuie și instalează aplicația, apoi verifică faptul că aceasta apare în spațiul de lucru:
```ts src/__tests__/app-install.integration-test.ts
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
const APP_PATH = process.cwd();
describe('App installation', () => {
beforeAll(async () => {
const buildResult = await appBuild({
appPath: APP_PATH,
tarball: true,
onProgress: (message: string) => console.log(`[build] ${message}`),
});
if (!buildResult.success) {
throw new Error(`Build failed: ${buildResult.error?.message}`);
}
const deployResult = await appDeploy({
tarballPath: buildResult.data.tarballPath!,
onProgress: (message: string) => console.log(`[deploy] ${message}`),
});
if (!deployResult.success) {
throw new Error(`Deploy failed: ${deployResult.error?.message}`);
}
const installResult = await appInstall({ appPath: APP_PATH });
if (!installResult.success) {
throw new Error(`Install failed: ${installResult.error?.message}`);
}
});
afterAll(async () => {
await appUninstall({ appPath: APP_PATH });
});
it('should find the installed app in the workspace', async () => {
const metadataClient = new MetadataApiClient();
const result = await metadataClient.query({
findManyApplications: {
id: true,
name: true,
universalIdentifier: true,
},
});
const installedApp = result.findManyApplications.find(
(app: { universalIdentifier: string }) =>
app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
);
expect(installedApp).toBeDefined();
});
});
```
### Rularea testelor
Asigurați-vă că serverul Twenty local rulează, apoi:
```bash filename="Terminal"
yarn test
```
Sau în modul watch în timpul dezvoltării:
```bash filename="Terminal"
yarn test:watch
```
### Verificarea tipurilor
Puteți rula și verificarea tipurilor pe aplicație fără a rula testele:
```bash filename="Terminal"
yarn twenty typecheck
```
Aceasta rulează `tsc --noEmit` și raportează orice erori de tip.
## Referință CLI
Dincolo de `dev`, `build`, `add` și `typecheck`, CLI oferă comenzi pentru executarea funcțiilor, vizualizarea jurnalelor și gestionarea instalărilor de aplicații.
### Executarea funcțiilor (`yarn twenty exec`)
Rulați manual o funcție logică fără a o declanșa prin HTTP, cron sau eveniment de bază de date:
```bash filename="Terminal"
# Execute by function name
yarn twenty exec -n create-new-post-card
# Execute by universalIdentifier
yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
# Pass a JSON payload
yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}'
# Execute the post-install function
yarn twenty exec --postInstall
```
### Vizualizarea jurnalelor funcțiilor (`yarn twenty logs`)
Transmiteți în flux jurnalele de execuție pentru funcțiile logice ale aplicației:
```bash filename="Terminal"
# Stream all function logs
yarn twenty logs
# Filter by function name
yarn twenty logs -n create-new-post-card
# Filter by universalIdentifier
yarn twenty logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
```
<Note>
Acest lucru este diferit de `yarn twenty server logs`, care afișează jurnalele containerului Docker. `yarn twenty logs` afișează jurnalele de execuție ale funcțiilor aplicației de pe serverul Twenty.
</Note>
### Dezinstalarea unei aplicații (`yarn twenty uninstall`)
Eliminați aplicația din spațiul de lucru activ:
```bash filename="Terminal"
yarn twenty uninstall
# Skip the confirmation prompt
yarn twenty uninstall --yes
```
## Gestionarea remote-urilor
Un „remote” este un server Twenty la care se conectează aplicația. În timpul configurării, Scaffolderul creează automat unul pentru dvs. Puteți adăuga mai multe remote-uri sau comuta între ele oricând.
```bash filename="Terminal"
# Add a new remote (opens a browser for OAuth login)
yarn twenty remote add
# Connect to a local Twenty server (auto-detects port 2020 or 3000)
yarn twenty remote add --local
# Add a remote non-interactively (useful for CI)
yarn twenty remote add --api-url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote
# List all configured remotes
yarn twenty remote list
# Switch the active remote
yarn twenty remote switch <name>
```
Acreditările dvs. sunt stocate în `~/.twenty/config.json`.
## CI cu GitHub Actions
Scaffolderul generează un workflow GitHub Actions gata de utilizare în `.github/workflows/ci.yml`. Rulează automat testele de integrare la fiecare push pe `main` și la pull request-uri.
Workflow-ul:
1. Preia codul
2. Pornește un server Twenty temporar folosind acțiunea `twentyhq/twenty/.github/actions/spawn-twenty-docker-image`
3. Instalează dependențele cu `yarn install --immutable`
4. Rulează `yarn test` cu `TWENTY_API_URL` și `TWENTY_API_KEY` injectate din rezultatele acțiunii
```yaml .github/workflows/ci.yml
name: CI
on:
push:
branches:
- main
pull_request: {}
env:
TWENTY_VERSION: latest
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Spawn Twenty instance
id: twenty
uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main
with:
twenty-version: ${{ env.TWENTY_VERSION }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Enable Corepack
run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- name: Install dependencies
run: yarn install --immutable
- name: Run integration tests
run: yarn test
env:
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }}
```
Nu trebuie să configurați niciun secret — acțiunea `spawn-twenty-docker-image` pornește un server Twenty efemer direct în runner și oferă detaliile de conectare. Secretul `GITHUB_TOKEN` este furnizat automat de GitHub.
Pentru a fixa o versiune Twenty specifică în loc de `latest`, modificați variabila de mediu `TWENTY_VERSION` din partea de sus a workflow-ului.
@@ -1,193 +0,0 @@
---
title: Conexiuni
description: Permite aplicației tale să acționeze în numele unui utilizator în servicii ale terților prin OAuth.
icon: plug
---
Conexiunile sunt acreditări pe care un utilizator le deține pentru un serviciu extern (Linear, GitHub, Slack, ...). Aplicația ta declară **cum** sunt obținute acele acreditări — un **furnizor de conexiune** — și le folosește în timpul execuției pentru a efectua apeluri autentificate către API-ul terț.
În prezent este acceptat doar OAuth 2.0. Tipurile viitoare de acreditări (jetoane de acces personale, chei API, autentificare de bază) se vor integra în aceeași interfață — aplicațiile care deja folosesc `defineConnectionProvider({ type: 'oauth', ... })` nu vor trebui să migreze.
<AccordionGroup>
<Accordion title="defineConnectionProvider" description="Declară cum sunt obținute conexiunile aplicației tale">
Un furnizor de conexiune descrie handshake-ul OAuth de care are nevoie aplicația ta. Utilizatorul face clic pe "Adaugă conexiune" în setările aplicației tale, completează ecranul de consimțământ al furnizorului și este creată o înregistrare `ConnectedAccount` în spațiul său de lucru.
O configurație funcțională are nevoie de **două fișiere** — furnizorul de conexiune și o declarație `serverVariables` corespunzătoare în `defineApplication` care conține acreditările clientului 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,
},
},
});
```
Puncte cheie:
* `name` este șirul identificator unic folosit în `listConnections({ providerName })` (kebab-case, trebuie să corespundă `^[a-z][a-z0-9-]*$`).
* `displayName` apare în fila de setări a aplicației și în lista de instrumente AI.
* `clientIdVariable` / `clientSecretVariable` sunt **nume**, nu valori — trebuie să se potrivească cheilor declarate în `defineApplication.serverVariables`. Valorile reale `client_id` și `client_secret` sunt introduse de administratorul serverului prin interfața de înregistrare a aplicației și nu sunt niciodată comise în repo-ul tău.
* Folosește `serverVariables` (nu `applicationVariables`) — acreditările OAuth sunt la nivel de server și există o singură aplicație OAuth pentru fiecare server Twenty.
* Până când ambele `serverVariables` sunt completate, fila de setări a aplicației afișează un indiciu "necesită administrator de server" și butonul "Adaugă conexiune" este dezactivat.
* `type: 'oauth'` este singura valoare acceptată în prezent. Discriminatorul este compatibil cu versiuni viitoare: tipurile viitoare (`'pat'`, `'api-key'`, ...) vor adăuga blocuri noi de sub-configurație alături de `oauth`.
URL-ul de callback OAuth pe care furnizorul tău trebuie să îl includă pe lista albă este:
```
https://<your-twenty-server>/apps/oauth/callback
```
</Accordion>
<Accordion title="listConnections / getConnection" description="Folosește conexiunile dintr-o funcție logică">
În interiorul unui handler de funcție logică, `listConnections({ providerName })` returnează înregistrările `ConnectedAccount` ale acestei aplicații pentru furnizorul dat, cu tokenuri de acces reîmprospătate.
```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 };
};
```
Fiecare conexiune are:
| Câmp | Descriere |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | ID unic al înregistrării; pasează-l la `getConnection(id)` pentru a reobține acea înregistrare |
| `visibility` | `'user'` (privată pentru un membru al spațiului de lucru) sau `'workspace'` (partajată cu toți membrii) |
| `scopes` | Permisiunile OAuth acordate de furnizorul upstream (distincte de `visibility` — nu au legătură) |
| `userWorkspaceId` | ID-ul userWorkspace al deținătorului — util pentru a alege "conexiunea utilizatorului care face cererea" în declanșatoarele de rută HTTP |
| `accessToken` | Token de acces OAuth proaspăt (reîmprospătat automat dacă a expirat) |
| `name` / `handle` | Numele afișat al conexiunii (derivat automat la callback-ul OAuth, poate fi redenumit de utilizator) |
| `authFailedAt` | Setat când cea mai recentă reîmprospătare a eșuat; utilizatorul trebuie să se reconecteze |
Puncte cheie:
* Pasează `{ providerName }` pentru a filtra după furnizor; omite-l pentru a obține toate conexiunile pe care această aplicație le deține la toți furnizorii.
* Serverul reîmprospătează transparent tokenul de acces înainte de a returna. Handlerul tău vede întotdeauna un token utilizabil (sau `authFailedAt` setat).
* `getConnection(id)` este echivalentul pentru o singură înregistrare.
</Accordion>
<Accordion title="Vizibilitate per utilizator vs partajată la nivel de spațiu de lucru" description="Cum aleg utilizatorii între acreditări private și partajate">
Când un utilizator face clic pe "Adaugă conexiune", i se solicită să aleagă o vizibilitate:
* **Doar pentru mine** — acreditarea este privată pentru utilizatorul care se conectează. Orice funcție logică apelată în numele lor (declanșator de rută HTTP cu `isAuthRequired: true`) o vede; declanșatoarele cron și evenimentele din bază de date nu.
* **Partajată la nivel de spațiu de lucru** — orice membru al spațiului de lucru poate folosi acreditarea. Declanșatoarele cron / din bază de date o văd, de asemenea, deoarece nu au un utilizator al cererii.
Folosește-o pe cea potrivită pentru fiecare 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');
```
Sunt permise mai multe conexiuni per (utilizator, furnizor), astfel încât același utilizator poate avea "Personal Linear" și "Work Linear" una lângă alta.
</Accordion>
<Accordion title="Configurare unică a furnizorului" description="Înregistrează-ți aplicația OAuth la serviciul terț">
Pentru fiecare furnizor de conexiune, administratorul serverului trebuie mai întâi să înregistreze o aplicație OAuth la serviciul terț.
1. Mergi la setările pentru dezvoltatori ale furnizorului (de ex. https://linear.app/settings/api/applications/new).
2. Setează **Redirect URI** la `\<SERVER_URL>/apps/oauth/callback`.
3. Copiază **Client ID** și **Client Secret** generate.
4. Deschide aplicația instalată în Twenty ca administrator de server → setează valorile pe `serverVariables` corespunzătoare.
5. Membrii spațiului de lucru pot apoi să adauge conexiuni din secțiunea **Conexiuni** a fiecărei aplicații.
</Accordion>
</AccordionGroup>
@@ -1,493 +0,0 @@
---
title: Model de date
description: Definiți obiecte, câmpuri, roluri și metadatele aplicației cu SDK-ul Twenty.
icon: database
---
Pachetul `twenty-sdk` furnizează funcții `defineEntity` pentru a declara modelul de date al aplicației dvs. Trebuie să folosiți `export default defineEntity({...})` pentru ca SDK-ul să detecteze entitățile. Aceste funcții validează configurația în timpul build-ului și oferă completare automată în IDE și siguranța tipurilor.
<Note>
**Organizarea fișierelor ține de dvs.**
Detectarea entităților este bazată pe AST — SDK-ul găsește apelurile `export default defineEntity(...)` indiferent unde se află fișierul. Gruparea fișierelor după tip (de exemplu, `logic-functions/`, `roles/`) este doar o convenție pentru organizarea codului, nu o cerință.
</Note>
<AccordionGroup>
<Accordion title="defineRole" description="Configurați permisiunile rolurilor și accesul la obiecte">
Rolurile încapsulează permisiuni asupra obiectelor și acțiunilor din spațiul dvs. de lucru.
```ts restricted-company-role.ts
import {
defineRole,
PermissionFlag,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
export default defineRole({
universalIdentifier: '2c80f640-2083-4803-bb49-003e38279de6',
label: 'My new role',
description: 'A role that can be used in your workspace',
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
},
],
fieldPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
fieldUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier,
canReadFieldValue: false,
canUpdateFieldValue: false,
},
],
permissionFlags: [PermissionFlag.APPLICATIONS],
});
```
</Accordion>
<Accordion title="defineApplication" description="Configurați metadatele aplicației (obligatoriu, una per aplicație)">
Fiecare aplicație trebuie să aibă exact un apel `defineApplication` care descrie:
* **Identitate**: identificatori, nume de afișare și descriere.
* **Permisiuni**: ce rol folosesc funcțiile și componentele front-end ale acesteia.
* **(Opțional) Variabile**: perechi cheievaloare expuse funcțiilor ca variabile de mediu.
* **(Opțional) funcții de pre-instalare / post-instalare**: funcții logice care rulează înainte sau după instalare.
```ts src/application-config.ts
import { defineApplication } from 'twenty-sdk/define';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
export default defineApplication({
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
displayName: 'My Twenty App',
description: 'My first Twenty app',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
description: 'Default recipient name for postcards',
value: 'Jane Doe',
isSecret: false,
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});
```
Notițe:
* Câmpurile `universalIdentifier` sunt ID-uri deterministe pe care le dețineți. Generați-le o singură dată și mențineți-le stabile între sincronizări.
* `applicationVariables` devin variabile de mediu pentru funcțiile și componentele front-end (de exemplu, `DEFAULT_RECIPIENT_NAME` este disponibil ca `process.env.DEFAULT_RECIPIENT_NAME`).
* `defaultRoleUniversalIdentifier` trebuie să facă referire la un rol definit cu `defineRole()` (vezi mai sus).
* Funcțiile de pre-instalare și post-instalare sunt detectate automat în timpul construirii manifestului — nu trebuie să le referiți în `defineApplication()`.
#### Metadate pentru marketplace
Dacă intenționați să [publicați aplicația](/l/ro/developers/extend/apps/publishing), aceste câmpuri opționale controlează modul în care apare în marketplace:
| Câmp | Descriere |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
| `autor` | Numele autorului sau al companiei |
| `categorie` | Categoria aplicației pentru filtrarea în marketplace |
| `logoUrl` | Calea către logo-ul aplicației (de ex., `public/logo.png`) |
| `screenshots` | Array de căi către capturi de ecran (de ex., `public/screenshot-1.png`) |
| `aboutDescription` | Descriere markdown mai lungă pentru fila "About". Dacă este omis, marketplace-ul folosește `README.md` al pachetului de pe npm |
| `websiteUrl` | Link către site-ul dvs. |
| `termsUrl` | Link către termenii de serviciu |
| `emailSupport` | Adresă de e-mail pentru suport |
| `issueReportUrl` | Link către sistemul de urmărire a problemelor |
#### Roluri și permisiuni
Câmpul `defaultRoleUniversalIdentifier` din `application-config.ts` desemnează rolul implicit utilizat de funcțiile logice și componentele front-end ale aplicației. Consultați `defineRole` mai sus pentru detalii.
* Tokenul de runtime injectat ca `TWENTY_APP_ACCESS_TOKEN` este derivat din acest rol.
* Clientul tipizat este restricționat la permisiunile acordate acelui rol.
* Respectați principiul celui mai mic privilegiu: creați un rol dedicat doar cu permisiunile de care au nevoie funcțiile.
##### Rol implicit pentru funcții
Când generați o aplicație nouă, CLI creează un fișier de rol implicit:
```ts src/roles/default-role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk/define';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
export default defineRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
objectPermissions: [],
fieldPermissions: [],
permissionFlags: [],
});
```
`universalIdentifier` al acestui rol este apoi referențiat în `application-config.ts` ca `defaultRoleUniversalIdentifier`.
* **\*.role.ts** definește ce poate face rolul.
* **application-config.ts** indică acel rol, astfel încât funcțiile moștenesc permisiunile lui.
Notițe:
* Porniți de la rolul generat, apoi restrângeți-l progresiv urmând principiul celui mai mic privilegiu.
* Înlocuiți `objectPermissions` și `fieldPermissions` cu obiectele și câmpurile de care au nevoie efectiv funcțiile.
* `permissionFlags` controlează accesul la capabilități la nivelul platformei. Mențineți-le la minimum.
* Vedeți un exemplu funcțional: [`hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts).
</Accordion>
<Accordion title="defineObject" description="Definiți obiecte personalizate cu câmpuri">
Obiectele personalizate descriu atât schema, cât și comportamentul înregistrărilor din spațiul dvs. de lucru. Utilizați `defineObject()` pentru a defini obiecte cu validare încorporată:
```ts postCard.object.ts
import { defineObject, FieldType } from 'twenty-sdk/define';
enum PostCardStatus {
DRAFT = 'DRAFT',
SENT = 'SENT',
DELIVERED = 'DELIVERED',
RETURNED = 'RETURNED',
}
export default defineObject({
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
nameSingular: 'postCard',
namePlural: 'postCards',
labelSingular: 'Post Card',
labelPlural: 'Post Cards',
description: 'A post card object',
icon: 'IconMail',
fields: [
{
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
name: 'content',
type: FieldType.TEXT,
label: 'Content',
description: "Postcard's content",
icon: 'IconAbc',
},
{
universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
name: 'recipientName',
type: FieldType.FULL_NAME,
label: 'Recipient name',
icon: 'IconUser',
},
{
universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
name: 'recipientAddress',
type: FieldType.ADDRESS,
label: 'Recipient address',
icon: 'IconHome',
},
{
universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
name: 'status',
type: FieldType.SELECT,
label: 'Status',
icon: 'IconSend',
defaultValue: `'${PostCardStatus.DRAFT}'`,
options: [
{ value: PostCardStatus.DRAFT, label: 'Draft', position: 0, color: 'gray' },
{ value: PostCardStatus.SENT, label: 'Sent', position: 1, color: 'orange' },
{ value: PostCardStatus.DELIVERED, label: 'Delivered', position: 2, color: 'green' },
{ value: PostCardStatus.RETURNED, label: 'Returned', position: 3, color: 'orange' },
],
},
{
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
name: 'deliveredAt',
type: FieldType.DATE_TIME,
label: 'Delivered at',
icon: 'IconCheck',
isNullable: true,
defaultValue: null,
},
],
});
```
Puncte cheie:
* Folosiți `defineObject()` pentru validare încorporată și suport mai bun în IDE.
* `universalIdentifier` trebuie să fie unic și stabil între implementări.
* Fiecare câmp necesită un `name`, un `type`, un `label` și propriul `universalIdentifier` stabil.
* Matricea `fields` este opțională — puteți defini obiecte fără câmpuri personalizate.
* Puteți genera obiecte noi folosind `yarn twenty add`, care vă ghidează prin denumire, câmpuri și relații.
<Note>
**Câmpurile de bază sunt create automat.** Când definiți un obiect personalizat, Twenty adaugă automat câmpuri standard
precum `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` și `deletedAt`.
Nu trebuie să le definiți în tabloul `fields` — adăugați doar câmpurile personalizate proprii.
Puteți suprascrie câmpurile implicite definind un câmp cu același nume în tabloul `fields`,
dar acest lucru nu este recomandat.
</Note>
</Accordion>
<Accordion title="defineField — Câmpuri standard" description="Extindeți obiectele existente cu câmpuri suplimentare">
Utilizați `defineField()` pentru a adăuga câmpuri la obiecte pe care nu le dețineți — cum ar fi obiectele standard Twenty (Person, Company etc.). sau obiecte din alte aplicații. Spre deosebire de câmpurile inline din `defineObject()`, câmpurile independente necesită un `objectUniversalIdentifier` pentru a specifica obiectul pe care îl extind:
```ts src/fields/company-loyalty-tier.field.ts
import { defineField, FieldType } from 'twenty-sdk/define';
export default defineField({
universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890',
objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object
name: 'loyaltyTier',
type: FieldType.SELECT,
label: 'Loyalty Tier',
icon: 'IconStar',
options: [
{ value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' },
{ value: 'SILVER', label: 'Silver', position: 1, color: 'gray' },
{ value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' },
],
});
```
Puncte cheie:
* `objectUniversalIdentifier` identifică obiectul țintă. Pentru obiectele standard, utilizați `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` exportați din `twenty-sdk`.
* Atunci când definiți câmpuri inline în `defineObject()`, nu aveți nevoie de `objectUniversalIdentifier` — este moștenit de la obiectul părinte.
* `defineField()` este singura modalitate de a adăuga câmpuri la obiecte pe care nu le-ați creat cu `defineObject()`.
</Accordion>
<Accordion title="defineField — Câmpuri de relație" description="Conectați obiectele între ele cu relații bidirecționale">
Relațiile conectează obiectele între ele. În Twenty, relațiile sunt întotdeauna bidirecționale — definiți ambele părți, iar fiecare parte o referențiază pe cealaltă.
Există două tipuri de relații:
| Tip relație | Descriere | Are cheie străină? |
| ------------- | ---------------------------------------------------------------------------------- | --------------------- |
| `MANY_TO_ONE` | Multe înregistrări ale acestui obiect indică către o singură înregistrare a țintei | Da (`joinColumnName`) |
| `ONE_TO_MANY` | O înregistrare a acestui obiect are multe înregistrări ale țintei | Nu (partea inversă) |
#### Cum funcționează relațiile
Fiecare relație necesită **două câmpuri** care se referențiază reciproc:
1. Partea **MANY_TO_ONE** — se află pe obiectul care deține cheia străină
2. Partea **ONE_TO_MANY** — se află pe obiectul care deține colecția
Ambele câmpuri folosesc `FieldType.RELATION` și se referențiază încrucișat prin `relationTargetFieldMetadataUniversalIdentifier`.
#### Exemplu: Post Card are mulți destinatari
Presupuneți că un `PostCard` poate fi trimis către multe înregistrări `PostCardRecipient`. Fiecare destinatar aparține exact unui Post Card.
**Pasul 1: Definiți partea ONE_TO_MANY pe PostCard** (partea "one"):
```ts src/fields/post-card-recipients-on-post-card.field.ts
import { defineField, FieldType, RelationType } from 'twenty-sdk/define';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111';
// Import from the other side
import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field';
export default defineField({
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCardRecipients',
label: 'Post Card Recipients',
icon: 'IconUsers',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
});
```
**Pasul 2: Definiți partea MANY_TO_ONE pe PostCardRecipient** (partea "many" — deține cheia străină):
```ts src/fields/post-card-on-post-card-recipient.field.ts
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk/define';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222';
// Import from the other side
import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field';
export default defineField({
universalIdentifier: POST_CARD_FIELD_ID,
objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
icon: 'IconMail',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
});
```
<Note>
**Importuri circulare:** Ambele câmpuri de relație se referă unul la celălalt prin `universalIdentifier`. Pentru a evita problemele de import circular, exportați ID-urile câmpurilor ca constante denumite din fiecare fișier și importați-le în celălalt fișier. Sistemul de build le rezolvă în timpul compilării.
</Note>
#### Relaționarea cu obiectele standard
Pentru a crea o relație cu un obiect Twenty încorporat (Person, Company etc.), utilizați `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`:
```ts src/fields/person-on-self-hosting-user.field.ts
import {
defineField,
FieldType,
RelationType,
OnDeleteAction,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object';
export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333';
export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444';
export default defineField({
universalIdentifier: PERSON_FIELD_ID,
objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'person',
label: 'Person',
description: 'Person matching with the self hosting user',
isNullable: true,
relationTargetObjectMetadataUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'personId',
},
});
```
#### Proprietăți ale câmpului de relație
| Proprietate | Obligatoriu | Descriere |
| ------------------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------- |
| `tip` | Da | Trebuie să fie `FieldType.RELATION` |
| `relationTargetObjectMetadataUniversalIdentifier` | Da | `universalIdentifier` al obiectului țintă |
| `relationTargetFieldMetadataUniversalIdentifier` | Da | `universalIdentifier` al câmpului corespunzător de pe obiectul țintă |
| `universalSettings.relationType` | Da | `RelationType.MANY_TO_ONE` sau `RelationType.ONE_TO_MANY` |
| `universalSettings.onDelete` | Doar MANY_TO_ONE | Ce se întâmplă atunci când înregistrarea referențiată este ștearsă: `CASCADE`, `SET_NULL`, `RESTRICT` sau `NO_ACTION` |
| `universalSettings.joinColumnName` | Doar MANY_TO_ONE | Numele coloanei din baza de date pentru cheia străină (de ex., `postCardId`) |
#### Câmpuri de relație inline în defineObject
Puteți defini, de asemenea, câmpuri de relație direct în `defineObject()`. În acest caz, omiteți `objectUniversalIdentifier` — este moștenit de la obiectul părinte:
```ts
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCardRecipient',
// ...
fields: [
{
universalIdentifier: POST_CARD_FIELD_ID,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
},
// ... other fields
],
});
```
</Accordion>
</AccordionGroup>
## Generarea scheletului entităților cu `yarn twenty add`
În loc să creați manual fișiere de entități, puteți folosi generatorul interactiv (scaffolder):
```bash filename="Terminal"
yarn twenty add
```
Acesta vă solicită să alegeți un tip de entitate și vă ghidează prin câmpurile necesare. Generează un fișier gata de utilizare, cu un `universalIdentifier` stabil și apelul corect `defineEntity()`.
Puteți de asemenea să transmiteți direct tipul de entitate pentru a sări peste primul prompt:
```bash filename="Terminal"
yarn twenty add object
yarn twenty add logicFunction
yarn twenty add frontComponent
```
### Tipuri de entități disponibile
| Tipul entității | Comandă | Fișier generat |
| ---------------------------- | ------------------------------------ | ------------------------------------------------------- |
| Obiect | `yarn twenty add object` | `src/objects/\<name>.ts` |
| Câmp | `yarn twenty add field` | `src/fields/\<name>.ts` |
| Funcție logică | `yarn twenty add logicFunction` | `src/logic-functions/\<name>.ts` |
| Componentă frontend | `yarn twenty add frontComponent` | `src/front-components/\<name>.tsx` |
| Rol | `yarn twenty add role` | `src/roles/\<name>.ts` |
| Abilitate | `yarn twenty add skill` | `src/skills/\<name>.ts` |
| Agent | `yarn twenty add agent` | `src/agents/\<name>.ts` |
| Vizualizare | `yarn twenty add view` | `src/views/\<name>.ts` |
| Element de meniu de navigare | `yarn twenty add navigationMenuItem` | `src/navigation-menu-items/\<name>.ts` |
| Machetă de pagină | `yarn twenty add pageLayout` | `src/page-layouts/\<name>.ts` |
### Ce generează scaffolder-ul
Fiecare tip de entitate are propriul său șablon. De exemplu, `yarn twenty add object` solicită:
1. **Nume (singular)** — de ex., `invoice`
2. **Nume (plural)** — de ex., `invoices`
3. **Etichetă (singular)** — completată automat din nume (de ex., `Invoice`)
4. **Etichetă (plural)** — completată automat (de ex., `Invoices`)
5. **Creați o vizualizare și un element de navigare?** — dacă răspundeți afirmativ, scaffolder-ul generează, de asemenea, o vizualizare corespunzătoare și un link în bara laterală pentru noul obiect.
Alte tipuri de entități au prompturi mai simple — majoritatea cer doar un nume.
Tipul de entitate `field` este mai detaliat: solicită numele câmpului, eticheta, tipul (dintr-o listă cu toate tipurile de câmp disponibile precum `TEXT`, `NUMBER`, `SELECT`, `RELATION` etc.) și `universalIdentifier` al obiectului țintă.
### Cale de output personalizată
Utilizați opțiunea `--path` pentru a plasa fișierul generat într-o locație personalizată:
```bash filename="Terminal"
yarn twenty add logicFunction --path src/custom-folder
```
@@ -1,489 +0,0 @@
---
title: Componente front-end
description: Construiți componente React care se afișează în interfața Twenty, cu izolare în sandbox.
icon: window-maximize
---
Componentele front-end sunt componente React care se afișează direct în interfața Twenty. Rulează într-un **Web Worker** izolat folosind Remote DOM — codul este izolat (sandboxed), dar se redă nativ în pagină, nu într-un iframe.
## Unde pot fi utilizate componentele frontale
Componentele frontale pot fi afișate în două locații în cadrul Twenty:
* **Panou lateral** — Componentele frontale care nu sunt headless se deschid în panoul lateral din dreapta. Acesta este comportamentul implicit atunci când o componentă frontală este declanșată din meniul de comenzi.
* **Widgeturi (tablouri de bord și pagini de înregistrare)** — Componentele frontale pot fi încorporate ca widgeturi în machetele de pagină. La configurarea unui tablou de bord sau a machetei unei pagini de înregistrare, utilizatorii pot adăuga un widget de componentă frontală.
## Exemplu de bază
Cel mai rapid mod de a vedea o componentă front-end în acțiune este să o înregistrați ca **element din meniul de comenzi**. Folosiți `defineCommandMenuItem` într-un fișier separat pentru ca componenta să apară ca buton de acțiune rapidă în colțul din dreapta sus al paginii:
```tsx src/front-components/hello-world.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
const HelloWorld = () => {
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h1>Hello from my app!</h1>
<p>This component renders inside Twenty.</p>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
name: 'hello-world',
description: 'A simple front component',
component: HelloWorld,
});
```
```ts src/command-menu-items/hello-world.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
shortLabel: 'Hello',
label: 'Hello World',
icon: 'IconBolt',
isPinned: true,
availabilityType: 'GLOBAL',
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
});
```
După sincronizarea cu `yarn twenty dev` (sau prin rularea comenzii `yarn twenty dev --once` o singură dată), acțiunea rapidă apare în colțul din dreapta sus al paginii:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="Buton de acțiune rapidă în colțul din dreapta sus" />
</div>
Faceți clic pe el pentru a afișa componenta inline.
## Câmpuri de configurare
| Câmp | Obligatoriu | Descriere |
| --------------------- | ----------- | --------------------------------------------------------------------------- |
| `universalIdentifier` | Da | ID unic stabil pentru această componentă |
| `component` | Da | O funcție de componentă React |
| `name` | Nu | Nume afișat |
| `description` | Nu | Descriere a ceea ce face componenta |
| `isHeadless` | Nu | Setați la `true` dacă componenta nu are interfață vizibilă (vedeți mai jos) |
## Plasarea unei componente front-end pe o pagină
Dincolo de comenzi, puteți încorpora o componentă front-end direct într-o pagină de înregistrare adăugând-o ca widget într-un **layout de pagină**. Consultați secțiunea [definePageLayout](/l/ro/developers/extend/apps/skills-and-agents#definepagelayout) pentru detalii.
## Headless vs non-headless
Componentele frontale au două moduri de randare controlate de opțiunea `isHeadless`:
**Non-headless (implicit)** — Componenta afișează o interfață vizibilă. Când este declanșat din meniul de comenzi, se deschide în panoul lateral. Acesta este comportamentul implicit când `isHeadless` este `false` sau omis.
**Headless (`isHeadless: true`)** — Componenta se montează invizibil în fundal. Nu deschide panoul lateral. Componentele headless sunt concepute pentru acțiuni care execută logică și apoi se demontează — de exemplu, rularea unei sarcini asincrone, navigarea la o pagină sau afișarea unui modal de confirmare. Se potrivesc în mod natural cu componentele Command din SDK descrise mai jos.
```tsx src/front-components/sync-tracker.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useRecordId, enqueueSnackbar } from 'twenty-sdk/front-component';
import { useEffect } from 'react';
const SyncTracker = () => {
const recordId = useRecordId();
useEffect(() => {
enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' });
}, [recordId]);
return null;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'sync-tracker',
description: 'Tracks record views silently',
isHeadless: true,
component: SyncTracker,
});
```
Deoarece componenta returnează `null`, Twenty omite redarea unui container pentru ea — nu apare spațiu gol în layout. Componenta are în continuare acces la toate hook-urile și la API-ul de comunicare cu gazda.
## Componentele Command din SDK
Pachetul `twenty-sdk` oferă patru componente ajutătoare Command, concepute pentru componente front-end headless. Fiecare componentă execută o acțiune la montare, gestionează erorile afișând o notificare snackbar și demontează automat componenta de interfață la final.
Importă-le din `twenty-sdk/command`:
* **`Command`** — Rulează un callback asincron prin prop-ul `execute`.
* **`CommandLink`** — Navighează către o rută a aplicației. Props: `to`, `params`, `queryParams`, `options`.
* **`CommandModal`** — Deschide un modal de confirmare. Dacă utilizatorul confirmă, execută callback-ul `execute`. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`.
* **`CommandOpenSidePanelPage`** — Deschide o anumită pagină din panoul lateral. Props: `page`, `pageTitle`, `pageIcon`.
Iată un exemplu complet de componentă front-end headless care folosește `Command` pentru a rula o acțiune din meniul de comenzi:
```tsx src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Command } from 'twenty-sdk/command';
import { CoreApiClient } from 'twenty-sdk/clients';
const RunAction = () => {
const execute = async () => {
const client = new CoreApiClient();
await client.mutation({
createTask: {
__args: { data: { title: 'Created by my app' } },
id: true,
},
});
};
return <Command execute={execute} />;
};
export default defineFrontComponent({
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
name: 'run-action',
description: 'Creates a task from the command menu',
component: RunAction,
isHeadless: true,
});
```
```ts src/command-menu-items/run-action.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
icon: 'IconPlayerPlay',
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
});
```
Și un exemplu care folosește `CommandModal` pentru a cere confirmarea înainte de execuție:
```tsx src/front-components/delete-draft.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { CommandModal } from 'twenty-sdk/command';
const DeleteDraft = () => {
const execute = async () => {
// perform the deletion
};
return (
<CommandModal
title="Delete draft?"
subtitle="This action cannot be undone."
execute={execute}
confirmButtonText="Delete"
confirmButtonAccent="danger"
/>
);
};
export default defineFrontComponent({
universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
name: 'delete-draft',
description: 'Deletes a draft with confirmation',
component: DeleteDraft,
isHeadless: true,
});
```
## Accesarea contextului de rulare
În interiorul componentei, folosiți hook-urile SDK pentru a accesa utilizatorul curent, înregistrarea curentă și instanța componentei:
```tsx src/front-components/record-info.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import {
useUserId,
useRecordId,
useFrontComponentId,
} from 'twenty-sdk/front-component';
const RecordInfo = () => {
const userId = useUserId();
const recordId = useRecordId();
const componentId = useFrontComponentId();
return (
<div>
<p>User: {userId}</p>
<p>Record: {recordId ?? 'No record context'}</p>
<p>Component: {componentId}</p>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012',
name: 'record-info',
component: RecordInfo,
});
```
Hook-uri disponibile:
| Hook | Returnează | Descriere |
| --------------------------------------------- | ------------------- | ----------------------------------------------------------------------------------- |
| `useUserId()` | `string` sau `null` | ID-ul utilizatorului curent |
| `useSelectedRecordIds()` | `string[]` | Toate ID-urile înregistrărilor selectate (array gol dacă nu este selectată niciuna) |
| `useRecordId()` | `string` sau `null` | **Învechit.** Folosiți `useSelectedRecordIds()` în schimb |
| `useFrontComponentId()` | `string` | ID-ul acestei instanțe de componentă |
| `useFrontComponentExecutionContext(selector)` | variază | Accesați întregul context de execuție cu o funcție selector |
## API-ul de comunicare cu gazda
Componentele front-end pot declanșa navigare, ferestre modale și notificări folosind funcții din `twenty-sdk`:
| Funcție | Descriere |
| ----------------------------------------------- | ----------------------------------- |
| `navigate(to, params?, queryParams?, options?)` | Navigați la o pagină din aplicație |
| `openSidePanelPage(params)` | Deschideți un panou lateral |
| `closeSidePanel()` | Închideți panoul lateral |
| `openCommandConfirmationModal(params)` | Afișați un dialog de confirmare |
| `enqueueSnackbar(params)` | Afișați o notificare tip toast |
| `unmountFrontComponent()` | Demontați componenta |
| `updateProgress(progress)` | Actualizați un indicator de progres |
Iată un exemplu care folosește API-ul gazdă pentru a afișa un snackbar și a închide panoul lateral după finalizarea unei acțiuni:
```tsx src/front-components/archive-record.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useRecordId } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-sdk/clients';
const ArchiveRecord = () => {
const recordId = useRecordId();
const handleArchive = async () => {
const client = new CoreApiClient();
await client.mutation({
updateTask: {
__args: { id: recordId, data: { status: 'ARCHIVED' } },
id: true,
},
});
await enqueueSnackbar({
message: 'Record archived',
variant: 'success',
});
await closeSidePanel();
};
return (
<div style={{ padding: '20px' }}>
<p>Archive this record?</p>
<button onClick={handleArchive}>Archive</button>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
name: 'archive-record',
description: 'Archives the current record',
component: ArchiveRecord,
});
```
### Lucrul cu mai multe înregistrări
Folosiți `useSelectedRecordIds()` pentru a gestiona mai multe înregistrări selectate. Acest lucru este util pentru operațiuni în masă:
```tsx src/front-components/bulk-export.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useSelectedRecordIds, numberOfSelectedRecords } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-sdk/clients';
const BulkExport = () => {
const selectedRecordIds = useSelectedRecordIds();
const handleExport = async () => {
const client = new CoreApiClient();
for (const recordId of selectedRecordIds) {
await client.mutation({
updateTask: {
__args: { id: recordId, data: { exported: true } },
id: true,
},
});
}
await enqueueSnackbar({
message: `Exported ${selectedRecordIds.length} records`,
variant: 'success',
});
await closeSidePanel();
};
return (
<div style={{ padding: '20px' }}>
<p>Export {selectedRecordIds.length} selected record(s)?</p>
<button onClick={handleExport}>Export</button>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
name: 'bulk-export',
description: 'Export selected records',
component: BulkExport,
command: {
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902',
label: 'Bulk Export',
availabilityType: 'RECORD_SELECTION',
conditionalAvailabilityExpression: numberOfSelectedRecords > 0,
},
});
```
## defineCommandMenuItem
Folosiți `defineCommandMenuItem` pentru a înregistra o componentă front-end în meniul de comenzi (Cmd+K). Dacă `isPinned` este `true`, apare și ca buton de acțiune rapidă în colțul din dreapta sus al paginii.
```ts src/command-menu-items/open-dashboard.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
label: 'Open Dashboard',
shortLabel: 'Dashboard',
icon: 'IconLayoutDashboard',
isPinned: true,
availabilityType: 'GLOBAL',
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
});
```
| Câmp | Obligatoriu | Descriere |
| --------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `universalIdentifier` | Da | ID unic stabil pentru comandă |
| `label` | Da | Etichetă completă afișată în meniul de comenzi (Cmd+K) |
| `frontComponentUniversalIdentifier` | Da | `universalIdentifier` al componentei front-end pe care această comandă o deschide |
| `shortLabel` | Nu | Etichetă mai scurtă afișată pe butonul de acțiune rapidă fixat |
| `icon` | Nu | Numele pictogramei afișat lângă etichetă (de ex. `'IconBolt'`, `'IconSend'`) |
| `isPinned` | Nu | Când este `true`, afișează comanda ca buton de acțiune rapidă în colțul din dreapta sus al paginii |
| `availabilityType` | Nu | Controlează unde apare comanda: `'GLOBAL'` (mereu disponibilă), `'RECORD_SELECTION'` (doar când sunt selectate înregistrări) sau `'FALLBACK'` (afișată când nicio altă comandă nu se potrivește) |
| `availabilityObjectUniversalIdentifier` | Nu | Restricționați comanda la paginile unui anumit tip de obiect (de ex., doar pe înregistrările Company) |
| `conditionalAvailabilityExpression` | Nu | O expresie booleană pentru a controla dinamic dacă comanda este vizibilă (vezi mai jos) |
## Expresii de disponibilitate condițională
Câmpul `conditionalAvailabilityExpression` vă permite să controlați când este vizibilă o comandă în funcție de contextul paginii curente. Importați variabile tipizate și operatori din `twenty-sdk` pentru a construi expresii:
```ts src/command-menu-items/bulk-update.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
import {
objectPermissions,
everyEquals,
} from 'twenty-sdk/front-component';
export default defineCommandMenuItem({
universalIdentifier: '...',
label: 'Bulk Update',
availabilityType: 'RECORD_SELECTION',
frontComponentUniversalIdentifier: '...',
conditionalAvailabilityExpression: everyEquals(
objectPermissions,
'canUpdateObjectRecords',
true,
),
});
```
**Variabile de context** — acestea reprezintă starea curentă a paginii:
| Variabilă | Tip | Descriere |
| ------------------------------ | --------- | ---------------------------------------------------------------------- |
| `pageType` | `string` | Tipul paginii curente (de ex. `'RecordIndexPage'`, `'RecordShowPage'`) |
| `isInSidePanel` | `boolean` | Dacă componenta este redată într-un panou lateral |
| `numberOfSelectedRecords` | `number` | Numărul de înregistrări selectate în prezent |
| `isSelectAll` | `boolean` | Dacă "select all" este activ |
| `selectedRecords` | `array` | Obiectele înregistrărilor selectate |
| `favoriteRecordIds` | `array` | ID-urile înregistrărilor marcate ca favorite |
| `objectPermissions` | `object` | Permisiuni pentru tipul de obiect curent |
| `targetObjectReadPermissions` | `object` | Permisiuni de citire pentru obiectul țintă |
| `targetObjectWritePermissions` | `object` | Permisiuni de scriere pentru obiectul țintă |
| `featureFlags` | `object` | Steaguri de caracteristici active |
| `objectMetadataItem` | `object` | Metadatele tipului de obiect curent |
| `hasAnySoftDeleteFilterOnView` | `boolean` | Dacă vizualizarea curentă are un filtru soft-delete |
**Operatori** — combinați variabilele în expresii booleene:
| Operator | Descriere |
| ----------------------------------- | ----------------------------------------------------------------------- |
| `isDefined(value)` | `true` dacă valoarea nu este null/undefined |
| `isNonEmptyString(value)` | `true` dacă valoarea este un șir nevid |
| `includes(array, value)` | `true` dacă array-ul conține valoarea |
| `includesEvery(array, prop, value)` | `true` dacă proprietatea fiecărui element include valoarea |
| `every(array, prop)` | `true` dacă proprietatea este truthy pentru fiecare element |
| `everyDefined(array, prop)` | `true` dacă proprietatea este definită pentru fiecare element |
| `everyEquals(array, prop, value)` | `true` dacă proprietatea este egală cu valoarea pentru fiecare element |
| `some(array, prop)` | `true` dacă proprietatea este truthy pe cel puțin un element |
| `someDefined(array, prop)` | `true` dacă proprietatea este definită pe cel puțin un element |
| `someEquals(array, prop, value)` | `true` dacă proprietatea este egală cu valoarea pe cel puțin un element |
| `someNonEmptyString(array, prop)` | `true` dacă proprietatea este un șir nevid pe cel puțin un element |
| `none(array, prop)` | `true` dacă proprietatea este falsy pentru fiecare element |
| `noneDefined(array, prop)` | `true` dacă proprietatea este nedefinită pentru fiecare element |
| `noneEquals(array, prop, value)` | `true` dacă proprietatea nu este egală cu valoarea pe niciun element |
## Resurse publice
Componentele front-end pot accesa fișiere din directorul `public/` al aplicației folosind `getPublicAssetUrl`:
```tsx
import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define';
const Logo = () => <img src={getPublicAssetUrl('logo.png')} alt="Logo" />;
export default defineFrontComponent({
universalIdentifier: '...',
name: 'logo',
component: Logo,
});
```
Consultați [secțiunea despre resurse publice](/l/ro/developers/extend/apps/cli-and-testing#public-assets-public-folder) pentru detalii.
## Stilizare
Componentele front-end acceptă mai multe abordări de stilizare. Puteți folosi:
* **Stiluri inline** — `style={{ color: 'red' }}`
* **Componente Twenty UI** — import din `twenty-sdk/ui` (Button, Tag, Status, Chip, Avatar și altele)
* **Emotion** — CSS-in-JS cu `@emotion/react`
* **Styled-components** — pattern-uri `styled.div`
* **Tailwind CSS** — clase utilitare
* **Orice bibliotecă CSS-in-JS** compatibilă cu React
```tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Button, Tag, Status } from 'twenty-sdk/ui';
const StyledWidget = () => {
return (
<div style={{ padding: '16px', display: 'flex', gap: '8px' }}>
<Button title="Click me" onClick={() => alert('Clicked!')} />
<Tag text="Active" color="green" />
<Status color="green" text="Online" />
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-567890123456',
name: 'styled-widget',
component: StyledWidget,
});
```
@@ -1,55 +1,54 @@
---
title: Începeți
icon: rocket
description: Creați prima dvs. aplicație Twenty în câteva minute.
---
<Warning>
Aplicațiile sunt în prezent în testare alfa. Caracteristica funcționează, dar este încă în dezvoltare.
</Warning>
## Ce sunt aplicațiile?
Aplicațiile vă permit să extindeți Twenty cu obiecte personalizate, câmpuri, funcții logice, componente front-end, abilități IA și altele — toate gestionate ca cod. În loc să configurați totul prin interfața de utilizator (UI), vă definiți modelul de date și logica în TypeScript și le implementați în unul sau mai multe spații de lucru.
## Cerințe
* **Node.js 24+** — [Descărcați](https://nodejs.org/)
* **Yarn 4** — vine împreună cu Node prin Corepack. Activați-l: `corepack enable`
* **Docker** — [Descărcați](https://www.docker.com/products/docker-desktop/). Necesar pentru a rula un server Twenty local. Omiteți dacă rulați deja Twenty în altă parte.
Înainte de a începe, asigurați-vă că următoarele sunt instalate pe calculatorul dvs.:
Crearea unei aplicații Twenty are trei faze. Generatorul de schelet le reduce la o singură comandă pe happy path, dar fiecare fază este un concept separat — când ceva eșuează, dacă știți în ce fază sunteți, știți ce trebuie să corectați.
* **Node.js 24+** — [Descărcați aici](https://nodejs.org/)
* **Yarn 4** — Vine împreună cu Node.js prin Corepack. Activați-l rulând `corepack enable`
* **Docker** — [Descărcați aici](https://www.docker.com/products/docker-desktop/). Necesar pentru a rula o instanță Twenty locală. Nu este necesar dacă aveți deja un server Twenty care rulează.
| Fază | Ce faceți | Instrument | Rezultat |
| ----------------------- | ------------------------------------------------ | ----------------------------- | ------------------------------ |
| **1. Creați scheletul** | Generați codul sursă al aplicației | `npx create-twenty-app` | Un proiect TypeScript pe disc |
| **2. Rulați un server** | Porniți un server Twenty cu care să sincronizați | Docker + `yarn twenty server` | O instanță Twenty care rulează |
| **3. Sincronizați** | Sincronizați în timp real codul cu serverul | `yarn twenty dev` | Modificările apar în UI |
## Creați prima dvs. aplicație
---
### Creați scheletul aplicației
## Faza 1 — Creați scheletul proiectului
Creați o nouă aplicație din șablon:
Deschideți un terminal și rulați:
```bash filename="Terminal"
npx create-twenty-app@latest my-twenty-app
```
Vi se va cere un nume și o descriere — apăsați **Enter** pentru valorile implicite. Aceasta generează un proiect TypeScript în `my-twenty-app/` cu un fișier inițial `application-config.ts`, un rol implicit, un flux de lucru CI și un test de integrare.
Vi se va cere să introduceți un nume și o descriere pentru aplicația dvs. Apăsați **Enter** pentru a accepta valorile implicite.
**După această fază:** aveți codul sursă al aplicației pe mașina dvs. Încă nu rulează — aceasta este Faza 2.
Aceasta creează un folder nou numit `my-twenty-app` cu tot ce aveți nevoie.
---
### Configurați o instanță Twenty locală
## Faza 2 — Rulați un server Twenty local
Aplicația are nevoie de un server Twenty cu care să se sincronizeze. Serverul este o instanță Twenty completă — UI, API GraphQL, PostgreSQL — care rulează local în Docker. Codul local încarcă definițiile pe acel server, făcându-le să apară în UI.
Generatorul de schelet vă propune să pornească unul pentru dvs.:
Generatorul de schelet va întreba:
> **Doriți să configurați o instanță Twenty locală?**
* **Yes (recomandat)** — descarcă imaginea Docker `twentycrm/twenty-app-dev` și o pornește pe portul `2020`. Asigurați-vă mai întâi că Docker rulează.
* **No** — alegeți această opțiune dacă aveți deja un server Twenty la care doriți să vă conectați. Îl puteți conecta ulterior cu `yarn twenty remote add`.
* **Tastați `yes`** (recomandat) — Aceasta descarcă imaginea Docker `twenty-app-dev` și pornește un server Twenty local pe portul `2020`. Asigurați-vă că Docker rulează înainte de a continua.
* **Tastați `no`** — Alegeți această opțiune dacă aveți deja un server Twenty care rulează local.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/start-instance.png" alt="Porniți instanța locală?" />
</div>
După ce serverul pornește, se deschide un browser pentru autentificare. Folosiți contul demo preconfigurat:
### Autentificați-vă în spațiul dvs. de lucru
În continuare, se va deschide o fereastră de browser cu pagina de autentificare Twenty. Autentificați-vă cu contul demo preconfigurat:
* **E-mail:** `tim@apple.dev`
* **Parolă:** `tim@apple.dev`
@@ -58,81 +57,89 @@ După ce serverul pornește, se deschide un browser pentru autentificare. Folosi
<img src="/images/docs/developers/extends/apps/login.png" alt="Ecranul de autentificare Twenty" />
</div>
Faceți clic pe **Authorize** pe ecranul următor — aceasta oferă CLI-ului acces la spațiul dvs. de lucru.
### Autorizați aplicația
După autentificare, veți vedea un ecran de autorizare. Acest lucru permite aplicației dvs. să interacționeze cu spațiul dvs. de lucru.
Faceți clic pe **Authorize** pentru a continua.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/authorize.png" alt="Ecranul de autorizare Twenty CLI" />
</div>
Terminalul va confirma că totul este configurat.
După autorizare, terminalul va confirma că totul este configurat.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/scaffolded.png" alt="Aplicația a fost creată cu succes" />
</div>
**După această fază:** aveți un server Twenty care rulează la [http://localhost:2020](http://localhost:2020), iar CLI-ul dvs. este autorizat să sincronizeze cu acesta.
### Începeți dezvoltarea
<Note>
Dacă Docker nu este instalat sau nu rulează, generatorul de schelet vă va indica comanda corectă de pornire pentru sistemul dvs. de operare. După ce Docker rulează, puteți continua cu `yarn twenty server start` — nu este nevoie să recreați scheletul.
</Note>
---
## Faza 3 — Sincronizați modificările
Aceasta este bucla internă în care veți petrece cea mai mare parte a timpului.
Intrați în noul folder al aplicației și porniți serverul de dezvoltare:
```bash filename="Terminal"
cd my-twenty-app
yarn twenty dev
```
Aceasta monitorizează `src/`, reconstruiește la fiecare modificare și sincronizează rezultatul pe server. Editați un fișier, salvați, iar în decurs de o secundă serverul reflectă modificarea. Veți vedea în terminal un panou de stare în timp real.
Acesta monitorizează fișierele sursă, reconstruiește la fiecare modificare și sincronizează automat aplicația cu serverul Twenty local. Ar trebui să vedeți în terminal un panou de stare în timp real.
Pentru un output mai detaliat (jurnale de build, cereri de sincronizare, urme ale erorilor), adăugați `--verbose`.
Pentru un output mai detaliat (jurnale de build, cereri de sincronizare, urme ale erorilor), folosiți opțiunea `--verbose`:
```bash filename="Terminal"
yarn twenty dev --verbose
```
<Warning>
Modul de dezvoltare este disponibil doar pe instanțele Twenty care rulează în modul development (`NODE_ENV=development`). Instanțele de producție resping cererile de sincronizare pentru dezvoltare. Folosiți `yarn twenty deploy` pentru a implementa pe serverele de producție — vedeți [Publicarea aplicațiilor](/l/ro/developers/extend/apps/publishing) pentru detalii.
</Warning>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/dev.png" alt="Ieșirea terminalului în modul de dezvoltare" />
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="Ieșirea terminalului în modul de dezvoltare" />
</div>
Deschideți [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer). Ar trebui să vedeți aplicația dvs. listată la **Your Apps**.
#### One-shot sync with `yarn twenty dev --once`
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-1.png" alt="Lista Your Apps care afișează My twenty app" />
</div>
Faceți clic pe **My twenty app** pentru a vedea **înregistrarea aplicației** — o înregistrare la nivel de server care descrie aplicația dvs. (nume, identificator, credențiale OAuth, sursă). O singură înregistrare poate fi instalată în mai multe spații de lucru pe același server.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-2.png" alt="Detalii despre înregistrarea aplicației" />
</div>
Faceți clic pe **View installed app** pentru a vedea instalarea în spațiul de lucru. Fila **About** afișează versiunea și opțiunile de administrare.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Aplicație instalată" />
</div>
**După această fază:** aveți o buclă de dezvoltare în timp real. Editați orice fișier în `src/` și acesta apare în UI.
### Sincronizare unică pentru CI și scripturi
Adăugați `--once` pentru a rula un singur build + sync și a ieși — același flux, fără watcher:
If you do not want a watcher running in the background (for example in a CI pipeline, a git hook, or a scripted workflow), pass the `--once` flag. It runs the same pipeline as `yarn twenty dev` — build manifest, bundle files, upload, sync, regenerate the typed API client — but **exits as soon as the sync completes**:
```bash filename="Terminal"
yarn twenty dev --once
```
| Comandă | Comportament | Când se folosește |
| ------------------------ | ------------------------------------------------------------------------------------ | --------------------------------------------------------------- |
| `yarn twenty dev` | Monitorizează și resincronizează la fiecare modificare. Rulează până când îl opriți. | Dezvoltare locală interactivă. |
| `yarn twenty dev --once` | Un singur build + sync, iese cu `0` la succes, `1` la eșec. | CI, hook-uri pre-commit, agenți AI, fluxuri de lucru scriptate. |
| Comandă | Comportament | When to use |
| ------------------------ | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `yarn twenty dev` | Watches your source files and re-syncs on every change. Keeps running until you stop it. | Interactive local development — you want the live status panel and instant feedback loop. |
| `yarn twenty dev --once` | Performs a single build + sync, then exits with code `0` on success or `1` on failure. | Scripts, CI, pre-commit hooks, AI agents, and any non-interactive workflow. |
Ambele moduri necesită un server în modul de dezvoltare și un remote autentificat.
Both modes require a Twenty server running in development mode and an authenticated remote — the same prerequisites apply.
<Warning>
Modul de dezvoltare este disponibil doar pe instanțele Twenty care rulează în modul development (`NODE_ENV=development`). Instanțele de producție resping cererile de sincronizare din modul de dezvoltare — folosiți `yarn twenty deploy` pentru a implementa pe serverele de producție. Consultați [Publicarea aplicațiilor](/l/ro/developers/extend/apps/publishing).
</Warning>
### Vedeți aplicația în Twenty
Deschideți [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer) în browser. Navigați la **Settings > Apps** și selectați fila **Developer**. Ar trebui să vedeți aplicația listată la **Your Apps**:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-1.png" alt="Lista Your Apps care afișează My twenty app" />
</div>
Faceți clic pe **My twenty app** pentru a deschide **înregistrarea aplicației**. O înregistrare este un element la nivel de server care descrie aplicația — numele, identificatorul unic, acreditările OAuth și sursa (locală, npm sau arhivă tar). Aceasta există pe server, nu în interiorul unui spațiu de lucru anume. Când instalați o aplicație într-un spațiu de lucru, Twenty creează o aplicație la nivelul spațiului de lucru care face referire la această înregistrare. O singură înregistrare poate fi instalată în mai multe spații de lucru pe același server.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-2.png" alt="Detalii despre înregistrarea aplicației" />
</div>
Faceți clic pe **View installed app** pentru a vedea aplicația instalată. Fila **About** afișează versiunea curentă și opțiunile de administrare:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Aplicație instalată — fila About" />
</div>
Comutați la fila **Content** pentru a vedea tot ceea ce oferă aplicația — obiecte, câmpuri, funcții logice și agenți:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-4.png" alt="Aplicație instalată — fila Content" />
</div>
Totul este gata! Editați orice fișier din `src/`, iar modificările vor fi preluate automat.
---
@@ -140,112 +147,102 @@ Modul de dezvoltare este disponibil doar pe instanțele Twenty care rulează în
Aplicațiile sunt compuse din **entități** — fiecare definită într-un fișier TypeScript cu un singur `export default`:
| Entitate | Ce face |
| --------------------------- | ----------------------------------------------------------------------------------------------- |
| **Obiecte și câmpuri** | Modele de date personalizate (carte poștală, factură etc.) cu câmpuri tipizate |
| **Funcții logice** | TypeScript pe server declanșat de rute HTTP, programări cron sau evenimente din baza de date |
| **Componente front-end** | Componente React care se afișează în UI-ul Twenty (panou lateral, widgeturi, meniul de comenzi) |
| **Abilități și agenți** | Capabilități AI — instrucțiuni reutilizabile și asistenți autonomi |
| **Vizualizări și navigare** | Vizualizări de listă preconfigurate și elemente de meniu în bara laterală |
| **Layouturi de pagină** | Pagini personalizate de detalii ale înregistrărilor cu file și widgeturi |
| Entitate | Ce face |
| --------------------------- | ----------------------------------------------------------------------------------------------------- |
| **Obiecte și câmpuri** | Definiți modele de date personalizate (cum ar fi Post Card, Invoice) cu câmpuri de tip definit |
| **Funcții logice** | Funcții TypeScript pe server declanșate de rute HTTP, programări cron sau evenimente din baza de date |
| **Componente front-end** | Componente React care se afișează în UI-ul Twenty (panou lateral, widgeturi, meniul de comenzi) |
| **Abilități și agenți** | Capabilități AI — instrucțiuni reutilizabile și asistenți autonomi |
| **Vizualizări și navigare** | Vizualizări de listă preconfigurate și elemente de meniu în bara laterală pentru obiectele dvs. |
| **Layouturi de pagină** | Pagini personalizate de detalii ale înregistrărilor cu file și widgeturi |
Referință completă: [Crearea aplicațiilor](/l/ro/developers/extend/apps/building).
Accesați [Construirea aplicațiilor](/l/ro/developers/extend/apps/building) pentru un ghid detaliat despre fiecare tip de entitate.
---
## Structura proiectului
Generatorul de schelete generează următoarea structură de fișiere:
```text filename="my-twenty-app/"
my-twenty-app/
package.json
yarn.lock
.gitignore
.nvmrc
.yarnrc.yml
.oxlintrc.json
tsconfig.json
tsconfig.spec.json # TypeScript config for tests
vitest.config.ts # Vitest test runner configuration
LLMS.md
README.md
.github/
└── workflows/
└── ci.yml # GitHub Actions CI workflow
public/ # Public assets (images, fonts, etc.)
src/
application-config.ts # Required — your app's entry point
default-role.ts # Permissions for logic functions
constants/
universal-identifiers.ts # Auto-generated UUIDs and metadata
__tests__/
setup-test.ts
app-install.integration-test.ts
.github/workflows/ci.yml # GitHub Actions
public/ # Static assets
vitest.config.ts # Test runner config
tsconfig.json, tsconfig.spec.json
.nvmrc, .yarnrc.yml, .oxlintrc.json
README.md, LLMS.md
├── application-config.ts # Required — main application configuration
├── default-role.ts # Default role for logic functions
├── constants/
└── universal-identifiers.ts # Auto-generated UUIDs and app metadata
└── __tests__/
├── setup-test.ts # Test setup (server health check, config)
└── app-install.integration-test.ts # Integration test
```
| Fișier / Folder | Scop |
| ---------------------------------------- | -------------------------------------------------------------------- |
| `src/application-config.ts` | **Necesar.** Fișierul principal de configurare pentru aplicație. |
| `src/default-role.ts` | Rol implicit care controlează la ce pot avea acces funcțiile logice. |
| `src/constants/universal-identifiers.ts` | UUID-uri generate automat și metadate (nume afișat, descriere). |
| `src/__tests__/` | Teste de integrare (configurare + test exemplu). |
| `public/` | Resurse statice (imagini, fonturi) servite împreună cu aplicația. |
### Pornind de la un exemplu
Folosiți `--example` pentru a începe cu un proiect mai complet (obiecte personalizate, câmpuri, funcții logice, componente front-end):
Pentru a porni de la un exemplu mai complet cu obiecte, câmpuri, funcții logice, componente front-end și altele, folosiți opțiunea `--example`:
```bash filename="Terminal"
npx create-twenty-app@latest my-twenty-app --example postcard
```
Exemplele se află în [twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples). Puteți, de asemenea, să creați scheletul entităților individuale într-un proiect existent cu `yarn twenty add` — vedeți [Crearea aplicațiilor](/l/ro/developers/extend/apps/building#scaffolding-entities-with-yarn-twenty-add).
Exemplele sunt preluate din directorul [twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples) de pe GitHub. Puteți, de asemenea, să creați scheletul entităților individuale într-un proiect existent cu `yarn twenty add` (consultați [Construirea aplicațiilor](/l/ro/developers/extend/apps/building#scaffolding-entities-with-yarn-twenty-add)).
---
### Fișiere cheie
## Gestionarea serverului local
| Fișier / Folder | Scop |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `package.json` | Declară numele aplicației, versiunea și dependențele. Include un script `twenty` astfel încât să puteți rula `yarn twenty help` pentru a vedea toate comenzile. |
| `src/application-config.ts` | **Necesar.** Fișierul principal de configurare pentru aplicație. |
| `src/default-role.ts` | Rol implicit care controlează la ce pot avea acces funcțiile logice. |
| `src/constants/universal-identifiers.ts` | UUID-uri generate automat și metadatele aplicației (nume afișat, descriere). |
| `src/__tests__/` | Teste de integrare (configurare + test exemplu). |
| `public/` | Resurse statice (imagini, fonturi) servite împreună cu aplicația. |
Folosiți `yarn twenty server` pentru a controla containerul Twenty local:
## Server de dezvoltare local
| Comandă | Ce face |
| -------------------------------------- | ------------------------------------------------------------ |
| `yarn twenty server start` | Pornește serverul (descarcă imaginea dacă este necesar) |
| `yarn twenty server start --port 3030` | Pornește pe un port personalizat |
| `yarn twenty server stop` | Oprește serverul (păstrează datele) |
| `yarn twenty server status` | Afișează URL-ul, versiunea și credențialele de autentificare |
| `yarn twenty server logs` | Transmite în flux jurnalele serverului |
| `yarn twenty server reset` | Șterge datele și pornește de la zero |
| `yarn twenty server upgrade` | Descarcă cea mai recentă imagine `twenty-app-dev` |
| `yarn twenty server upgrade 2.2.0` | Actualizează la o versiune specifică |
Generatorul de schelete a pornit deja un server Twenty local pentru dvs. Pentru a-l gestiona ulterior, folosiți `yarn twenty server`:
Datele persistă între reporniri în două volume Docker (`twenty-app-dev-data` pentru PostgreSQL, `twenty-app-dev-storage` pentru fișiere). Folosiți `reset` pentru a șterge totul.
| Comandă | Descriere |
| -------------------------------------- | ------------------------------------------------------------- |
| `yarn twenty server start` | Pornește serverul local (descarcă imaginea dacă este necesar) |
| `yarn twenty server start --port 3030` | Pornește pe un port personalizat |
| `yarn twenty server stop` | Oprește serverul (păstrează datele) |
| `yarn twenty server status` | Afișează starea serverului, URL-ul și acreditările |
| `yarn twenty server logs` | Transmite în flux jurnalele serverului |
| `yarn twenty server logs --lines 100` | Afișează ultimele 100 de linii de jurnal |
| `yarn twenty server reset` | Șterge toate datele și pornește de la zero |
### Actualizarea imaginii serverului
Datele sunt păstrate între reporniri în două volume Docker (`twenty-app-dev-data` pentru PostgreSQL, `twenty-app-dev-storage` pentru fișiere). Folosiți `reset` pentru a șterge totul și a porni de la zero.
`yarn twenty server upgrade` descarcă cea mai recentă imagine, compară digest-urile și recreează containerul doar dacă s-a schimbat ceva. Volumele de date sunt păstrate — doar containerul este înlocuit. Dacă a fost descărcată o imagine nouă și containerul rula, actualizarea pornește automat un container nou; rulați apoi `yarn twenty server start` pentru a aștepta până când devine funcțional.
```bash filename="Terminal"
yarn twenty server upgrade # Latest
yarn twenty server upgrade 2.2.0 # Specific version
```
Puteți verifica versiunea care rulează cu `yarn twenty server status` (aceasta afișează `APP_VERSION` încorporat în container).
### Rularea unei instanțe de test în paralel
Adăugați `--test` la orice comandă `server` pentru a gestiona o a doua instanță, complet izolată — utilă pentru teste de integrare sau pentru a experimenta fără a atinge datele principale de dezvoltare:
| Comandă | Ce face |
| ----------------------------------- | --------------------------------------------------- |
| `yarn twenty server start --test` | Pornește instanța de test (implicit pe portul 2021) |
| `yarn twenty server stop --test` | Opriți-o |
| `yarn twenty server status --test` | Afișați-i starea |
| `yarn twenty server logs --test` | Transmiteți în flux jurnalele sale |
| `yarn twenty server reset --test` | Ștergeți-i datele |
| `yarn twenty server upgrade --test` | Actualizați-i imaginea |
Instanța de test are propriul container (`twenty-app-dev-test`), propriile volume (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`) și propria configurație — rulează alături de instanța principală, fără conflicte. Combinați `--test` cu `--port` pentru a înlocui portul 2021.
---
<Note>
Serverul necesită ca **Docker** să ruleze. Dacă vedeți eroarea "Docker not running", asigurați-vă că Docker Desktop (sau demonul Docker) este pornit.
</Note>
## Configurare manuală (fără generator)
Săriți peste generatorul de schelet dacă adăugați SDK-ul într-un proiect existent:
Dacă preferați să configurați totul manual în loc să folosiți `create-twenty-app`, o puteți face în doi pași.
**1. Adăugați `twenty-sdk` și `twenty-client-sdk` ca dependențe:**
```bash filename="Terminal"
yarn add twenty-sdk twenty-client-sdk
```
Adăugați scriptul în `package.json`:
**2. Adăugați un script `twenty` în `package.json`:**
```json filename="package.json"
{
@@ -255,19 +252,19 @@ Adăugați scriptul în `package.json`:
}
```
Acum puteți rula `yarn twenty dev`, `yarn twenty server start` și restul.
Acum puteți rula `yarn twenty dev`, `yarn twenty help` și toate celelalte comenzi.
<Note>
Nu instalați `twenty-sdk` global — fixați-l per proiect astfel încât fiecare aplicație să folosească propria versiune.
Nu instalați `twenty-sdk` global. Folosiți-l întotdeauna ca dependență locală de proiect, astfel încât fiecare proiect să își poată fixa propria versiune.
</Note>
---
## Depanare
* **Erori Docker** — Asigurați-vă că Docker Desktop (sau daemonul) rulează înainte de `yarn twenty server start`. Mesajul de eroare va afișa comanda corectă de pornire pentru sistemul dvs. de operare.
* **Versiune Node greșită** — Aveți nevoie de 24+. Verificați cu `node -v`.
* **Lipsește Yarn 4** — Rulați `corepack enable`.
* **Dependențe nefuncționale** — `rm -rf node_modules && yarn install`.
Dacă întâmpinați probleme:
Blocat? Întrebați pe [Discordul Twenty](https://discord.com/channels/1130383047699738754/1130386664812982322).
* Asigurați-vă că Docker rulează înainte de a porni scaffolderul cu o instanță locală.
* Asigurați-vă că folosiți **Node.js 24+** (`node -v` pentru verificare).
* Asigurați-vă că **Corepack este activat** (`corepack enable`) astfel încât Yarn 4 să fie disponibil.
* Încercați să ștergeți `node_modules` și să rulați din nou `yarn install` dacă dependențele par deteriorate.
Încă aveți probleme? Cereți ajutor pe [Discordul Twenty](https://discord.com/channels/1130383047699738754/1130386664812982322).
@@ -1,178 +0,0 @@
---
title: Aspect
description: Definiți vizualizări, elemente de meniu de navigare și layouturi de pagină pentru a stabili modul în care aplicația dvs. se afișează în Twenty.
icon: table-columns
---
Entitățile de layout controlează modul în care aplicația dvs. apare în UI-ul Twenty — ce se află în bara laterală, care vizualizări salvate vin împreună cu aplicația și cum este aranjată pagina de detalii a unei înregistrări.
## Concepte de layout
| Concept | Ce controlează | Entitate |
| -------------------------------- | ------------------------------------------------------------------------------------------------- | -------------------------- |
| **Vizualizare** | O configurație salvată a unei liste pentru un obiect — câmpuri vizibile, ordine, filtre, grupuri | `defineView` |
| **Element de meniu de navigare** | Un element în bara laterală stângă care face legătura către o vizualizare sau un URL extern | `defineNavigationMenuItem` |
| **Layout pagină** | Filele și widgeturile care alcătuiesc pagina de detalii a unei înregistrări | `definePageLayout` |
| **Filă layout pagină** | O filă independentă atașată unui layout pagină existent (standard sau al propriei tale aplicații) | `definePageLayoutTab` |
Vizualizările, elementele de navigare și layouturile de pagină fac referire unele la altele prin `universalIdentifier`:
* Un **element de meniu de navigare** de tip `VIEW` indică către un identificator `defineView`, astfel încât linkul din bara laterală deschide acea vizualizare salvată.
* Un **layout de pagină** de tip `RECORD_PAGE` vizează un obiect și poate încorpora [componente frontale](/l/ro/developers/extend/apps/front-components) în filele sale ca widgeturi.
<AccordionGroup>
<Accordion title="defineView" description="Definește vizualizări salvate pentru obiecte">
Vizualizările sunt configurații salvate despre cum sunt afișate înregistrările unui obiect — inclusiv ce câmpuri sunt vizibile, ordinea lor și orice filtre sau grupuri aplicate. Utilizați `defineView()` pentru a livra vizualizări preconfigurate împreună cu aplicația:
```ts src/views/example-view.ts
import { defineView, ViewKey } from 'twenty-sdk/define';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
import { NAME_FIELD_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
export default defineView({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'All example items',
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
key: ViewKey.INDEX,
position: 0,
fields: [
{
universalIdentifier: 'f926bdb7-6af7-4683-9a09-adbca56c29f0',
fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
position: 0,
isVisible: true,
size: 200,
},
],
});
```
Puncte cheie:
* `objectUniversalIdentifier` specifică la ce obiect se aplică această vizualizare.
* `key` determină tipul vizualizării (de ex., `ViewKey.INDEX` pentru vizualizarea principală de listă).
* `fields` controlează ce coloane apar și ordinea acestora. Fiecare câmp face referire la un `fieldMetadataUniversalIdentifier`.
* Puteți defini, de asemenea, `filters`, `filterGroups`, `groups` și `fieldGroups` pentru configurații mai avansate.
* `position` controlează ordonarea atunci când există mai multe vizualizări pentru același obiect.
</Accordion>
<Accordion title="defineNavigationMenuItem" description="Definește linkuri de navigare în bara laterală">
Elementele de meniu de navigare adaugă intrări personalizate în bara laterală a spațiului de lucru. Utilizați `defineNavigationMenuItem()` pentru a crea linkuri către vizualizări, URL-uri externe sau obiecte:
```ts src/navigation-menu-items/example-navigation-menu-item.ts
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define';
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from '../views/example-view';
export default defineNavigationMenuItem({
universalIdentifier: '9327db91-afa1-41b6-bd9d-2b51a26efb4c',
name: 'example-navigation-menu-item',
icon: 'IconList',
color: 'blue',
position: 0,
type: NavigationMenuItemType.VIEW,
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
});
```
Puncte cheie:
* `type` determină la ce face trimitere elementul de meniu: `NavigationMenuItemType.VIEW` pentru o vizualizare salvată sau `NavigationMenuItemType.LINK` pentru un URL extern.
* Pentru link-uri către vizualizări, setați `viewUniversalIdentifier`. Pentru link-uri externe, setați `link`.
* `position` controlează ordonarea în bara laterală.
* `icon` și `color` (opțional) personalizează aspectul.
</Accordion>
<Accordion title="definePageLayout" description="Definiți machete de pagină personalizate pentru vizualizările de înregistrare">
Machetele de pagină vă permit să personalizați aspectul unei pagini de detalii a unei înregistrări — ce file apar, ce widgeturi sunt în fiecare filă și cum sunt aranjate. Utilizați `definePageLayout()` pentru a livra machete personalizate împreună cu aplicația:
```ts src/page-layouts/example-record-page-layout.ts
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
export default definePageLayout({
universalIdentifier: '203aeb94-6701-46d6-9af1-be2bbcc9e134',
name: 'Example Record Page',
type: 'RECORD_PAGE',
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
tabs: [
{
universalIdentifier: '6ed26b60-a51d-4ad7-86dd-1c04c7f3cac5',
title: 'Hello World',
position: 50,
icon: 'IconWorld',
layoutMode: PageLayoutTabLayoutMode.CANVAS,
widgets: [
{
universalIdentifier: 'aa4234e0-2e5f-4c02-a96a-573449e2351d',
title: 'Hello World',
type: 'FRONT_COMPONENT',
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier:
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
},
],
},
],
});
```
Puncte cheie:
* `type` este de obicei `'RECORD_PAGE'` pentru a personaliza vizualizarea de detaliu a unui obiect specific.
* `objectUniversalIdentifier` specifică la ce obiect se aplică această machetă.
* Fiecare `tab` definește o secțiune a paginii cu un `title`, `position` și `layoutMode` (`CANVAS` pentru layout liber).
* Fiecare `widget` dintr-o filă poate reda o componentă frontend, o listă de relații sau alte tipuri de widgeturi integrate.
* `position` pe file le controlează ordinea. Folosiți valori mai mari (de ex., 50) pentru a plasa filele personalizate după cele integrate.
</Accordion>
<Accordion title="definePageLayoutTab" description="Adaugă o filă la un layout de pagină existent">
`definePageLayoutTab` permite aplicației tale să atașeze o singură filă — cu widgeturi opționale — la un layout de pagină **existent**. Cel mai comun caz de utilizare este adăugarea unei file personalizate (de exemplu, o filă de analize sau o filă cu rezumat AI) la una dintre paginile de înregistrare predefinite ale Twenty sau la un layout de pagină pe care propria ta aplicație îl livrează deja.
Layoutul de pagină țintă trebuie să fie fie un layout de pagină Twenty **standard**, fie unul definit de **propria ta aplicație**; referințele între aplicații la layouturi de pagină deținute de o altă aplicație instalată nu sunt acceptate în prezent.
```ts src/page-layouts/example-extra-tab.ts
import {
definePageLayoutTab,
PageLayoutTabLayoutMode,
} from 'twenty-sdk/define';
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
const COMPANY_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER =
'20202020-ab01-4001-8001-c0aba11c0100';
export default definePageLayoutTab({
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001',
pageLayoutUniversalIdentifier:
COMPANY_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
title: 'Hello World',
position: 1000,
icon: 'IconWorld',
layoutMode: PageLayoutTabLayoutMode.CANVAS,
widgets: [
{
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000002',
title: 'Hello World',
type: 'FRONT_COMPONENT',
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier:
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
},
],
});
```
Puncte cheie:
* `pageLayoutUniversalIdentifier` este **necesar** când folosești `definePageLayoutTab` și trebuie să indice către un layout de pagină care există deja la momentul instalării (standard sau al aplicației tale). Când lipsește layoutul de pagină părinte, instalarea eșuează cu o eroare clară de validare.
* `widgets` sunt limitate doar la această filă — fac referire la componente front-end, vizualizări etc., exact ca widgeturile definite inline în `definePageLayout`.
* `position` controlează ordonarea în raport cu filele existente din layoutul țintă. Alege o valoare care să plaseze fila ta acolo unde dorești, relativ la filele predefinite.
* Folosește aceasta în loc de `definePageLayout` atunci când vrei doar să **adaugi** la un layout existent. Folosește `definePageLayout` când deții întregul layout (de obicei un `RECORD_PAGE` pentru un obiect pe care îl livrezi în aplicația ta sau un `STANDALONE_PAGE`).
</Accordion>
</AccordionGroup>
@@ -1,566 +0,0 @@
---
title: Funcții logice
description: Definește funcții TypeScript pe partea de server cu declanșatoare HTTP, cron și de evenimente din baza de date.
icon: bolt
---
Funcțiile de logică sunt funcții TypeScript pe partea de server care rulează pe platforma Twenty. Acestea pot fi declanșate de solicitări HTTP, programări cron sau evenimente din baza de date — și pot fi, de asemenea, expuse ca instrumente pentru agenți AI.
<AccordionGroup>
<Accordion title="defineLogicFunction" description="Definiți funcții logice și declanșatoarele acestora">
Fiecare fișier de funcție folosește `defineLogicFunction()` pentru a exporta o configurație cu un handler și declanșatoare opționale.
```ts src/logic-functions/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk/define';
import { CoreApiClient, type Person } from 'twenty-client-sdk/core';
const handler = async (params: RoutePayload) => {
const client = new CoreApiClient();
const name = 'name' in params.queryStringParameters
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: 'Hello world';
const result = await client.mutation({
createPostCard: {
__args: { data: { name } },
id: true,
name: true,
},
});
return result;
};
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'create-new-post-card',
timeoutSeconds: 2,
handler,
httpRouteTriggerSettings: {
path: '/post-card/create',
httpMethod: 'GET',
isAuthRequired: true,
},
/*databaseEventTriggerSettings: {
eventName: 'people.created',
},*/
/*cronTriggerSettings: {
pattern: '0 0 1 1 *',
},*/
});
```
Tipuri de declanșatoare disponibile:
* **httpRoute**: Expune funcția pe o cale și metodă HTTP **sub endpoint-ul `/s/`**:
> de ex. `path: '/post-card/create'` este apelabil la `https://your-twenty-server.com/s/post-card/create`
* **cron**: Rulează funcția pe un program folosind o expresie CRON.
* **databaseEvent**: Rulează la evenimentele ciclului de viață ale obiectelor din spațiul de lucru. Când operațiunea evenimentului este `updated`, câmpurile specifice de urmărit pot fi specificate în array-ul `updatedFields`. Dacă este lăsat nedefinit sau gol, orice actualizare va declanșa funcția.
> de ex. `person.updated`, `*.created`, `company.*`
<Note>
Puteți, de asemenea, să executați manual o funcție folosind CLI:
```bash filename="Terminal"
yarn twenty exec -n create-new-post-card -p '{"key": "value"}'
```
```bash filename="Terminal"
yarn twenty exec -y e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
```
Puteți urmări jurnalele cu:
```bash filename="Terminal"
yarn twenty logs
```
</Note>
#### Payload-ul declanșatorului de rută
Când un declanșator de rută invocă funcția logică, aceasta primește un obiect `RoutePayload` care urmează
[AWS HTTP API v2 format](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html).
Importați tipul `RoutePayload` din `twenty-sdk`:
```ts
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
const handler = async (event: RoutePayload) => {
const { headers, queryStringParameters, pathParameters, body } = event;
const { method, path } = event.requestContext.http;
return { message: 'Success' };
};
```
Tipul `RoutePayload` are următoarea structură:
| Proprietate | Tip | Descriere | Exemplu |
| ---------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `headers` | `Record\<string, string \| undefined>` | Anteturi HTTP (doar cele listate în `forwardedRequestHeaders`) | consultați secțiunea de mai jos |
| `queryStringParameters` | `Record\<string, string \| undefined>` | Parametri query string (valorile multiple unite cu virgule) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
| `pathParameters` | `Record\<string, string \| undefined>` | Parametri de cale extrași din modelul rutei | `/users/:id`, `/users/123` -> `{ id: '123' }` |
| `body` | `object \| null` | Corpul cererii analizat (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
| `rawBody` | `string \| undefined` | Corpul original al cererii în UTF-8, înainte de parsarea JSON. Util pentru verificarea semnăturilor de tip HMAC pentru webhook-uri (de exemplu, `X-Hub-Signature-256` de la GitHub, Stripe). `undefined` atunci când mediul de execuție nu a păstrat-o. | |
| `isBase64Encoded` | `boolean` | Indică dacă corpul este codificat în base64 | |
| `requestContext.http.method` | `string` | Metoda HTTP (GET, POST, PUT, PATCH, DELETE) | |
| `requestContext.http.path` | `string` | Calea brută a cererii | |
#### forwardedRequestHeaders
În mod implicit, anteturile HTTP din cererile de intrare **nu** sunt transmise funcției dvs. de logică din motive de securitate.
Pentru a accesa anumite anteturi, listați-le explicit în array-ul `forwardedRequestHeaders`:
```ts
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'webhook-handler',
handler,
httpRouteTriggerSettings: {
path: '/webhook',
httpMethod: 'POST',
isAuthRequired: false,
forwardedRequestHeaders: ['x-webhook-signature', 'content-type'],
},
});
```
În handler, accesați anteturile transmise mai departe astfel:
```ts
const handler = async (event: RoutePayload) => {
const signature = event.headers['x-webhook-signature'];
const contentType = event.headers['content-type'];
// Validate webhook signature...
return { received: true };
};
```
<Note>
Numele anteturilor sunt normalizate la litere mici. Accesați-le folosind chei cu litere mici (de exemplu, `event.headers['content-type']`).
</Note>
#### Expunerea unei funcții ca instrument AI sau ca acțiune în fluxul de lucru
Funcțiile logice pot fi expuse în două locuri, fiecare cu propriul declanșator:
* **`toolTriggerSettings`** — face funcția descoperibilă de către funcționalitățile AI ale Twenty (chat, MCP, apelarea de funcții). Folosește JSON Schema standard, formatul pe care LLM-urile îl înțeleg nativ.
* **`workflowActionTriggerSettings`** — determină ca funcția să apară ca un pas în constructorul vizual de fluxuri de lucru. Folosește `InputSchema` bogat al Twenty, astfel încât constructorul să poată afișa editori de câmp adecvați, selectoare de variabile și etichete.
O funcție poate opta pentru una, cealaltă sau ambele. Acestea stau alături de `cronTriggerSettings`, `databaseEventTriggerSettings` și `httpRouteTriggerSettings` — același tipar, aceeași formă.
```ts src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
__args: {
data: {
title: `Enrich data for ${params.companyName}`,
body: `Domain: ${params.domain ?? 'unknown'}`,
},
},
id: true,
},
});
return { taskId: result.createTask.id };
};
export default defineLogicFunction({
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
name: 'enrich-company',
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
toolTriggerSettings: {},
});
```
Puncte cheie:
* O funcție poate combina suprafețele — declară atât `toolTriggerSettings`, cât și `workflowActionTriggerSettings` pentru a o expune atât în chat, cât și în constructorul de fluxuri de lucru.
* `toolTriggerSettings.inputSchema` și `workflowActionTriggerSettings.inputSchema` sunt ambele opționale. Când sunt omise, generatorul de manifest le deduce din codul sursă al handlerului (JSON Schema pentru instrumentul AI, `InputSchema` al Twenty pentru acțiunea de flux de lucru). Furnizează unul în mod explicit atunci când dorești o tipizare mai bogată — de exemplu, cu câmpuri compatibile cu `FieldMetadataType`, precum `CURRENCY` sau `RELATION` pentru constructorul de fluxuri de lucru, sau cu câmpuri `description` pe care agentul AI le poate citi:
```ts
export default defineLogicFunction({
...,
toolTriggerSettings: {
inputSchema: {
type: 'object',
properties: {
companyName: {
type: 'string',
description: 'The name of the company to enrich',
},
domain: {
type: 'string',
description: 'The company website domain (optional)',
},
},
required: ['companyName'],
},
},
});
```
<Note>
**Scrieți o `description` bună.** Agenții AI se bazează pe câmpul `description` al funcției pentru a decide când să folosească instrumentul. Fiți specifici cu privire la ceea ce face instrumentul și când ar trebui apelat.
</Note>
</Accordion>
<Accordion title="definePostInstallLogicFunction" description="Definește o funcție logică post-instalare (una per aplicație)">
O funcție post-instalare este o funcție logică care rulează automat după instalarea aplicației într-un spațiu de lucru. Serverul o execută **după** ce metadatele aplicației au fost sincronizate și clientul SDK a fost generat, astfel încât spațiul de lucru este complet pregătit pentru utilizare, iar noua schemă este disponibilă. Cazuri tipice de utilizare includ popularea cu date implicite, crearea de înregistrări inițiale, configurarea setărilor spațiului de lucru sau provizionarea resurselor în cadrul serviciilor terților.
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
const handler = async (payload: InstallPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
export default definePostInstallLogicFunction({
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
shouldRunSynchronously: false,
handler,
});
```
Puteți, de asemenea, să executați manual funcția post-instalare oricând folosind CLI:
```bash filename="Terminal"
yarn twenty exec --postInstall
```
Puncte cheie:
* Funcțiile de post-instalare folosesc `definePostInstallLogicFunction()` — o variantă specializată care omite setările de declanșare (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `toolTriggerSettings`, `workflowActionTriggerSettings`).
* Handlerul primește un `InstallPayload` cu `{ previousVersion?: string; newVersion: string }` — `newVersion` este versiunea care este instalată, iar `previousVersion` este versiunea instalată anterior (sau `undefined` la o instalare nouă). Folosiți aceste valori pentru a distinge instalările noi de actualizări și pentru a rula logică de migrare specifică versiunii.
* **Când rulează hook-ul**: doar la instalări noi, în mod implicit. Transmiteți `shouldRunOnVersionUpgrade: true` dacă doriți să ruleze și atunci când aplicația este actualizată de la o versiune anterioară. Când este omis, indicatorul are implicit valoarea `false`, iar actualizările sar peste hook.
* **Model de execuție — implicit asincron, sincron opțional**: indicatorul `shouldRunSynchronously` controlează *modul în care* este executat post-install.
* `shouldRunSynchronously: false` *(implicit)* — hook-ul este **pus în coadă în message queue** cu `retryLimit: 3` și rulează asincron într-un worker. Răspunsul la instalare revine imediat ce jobul este pus în coadă, astfel încât un handler lent sau care eșuează nu blochează apelantul. Workerul va reîncerca de până la trei ori. **Folosiți acest mod pentru joburi de lungă durată** — popularea unor seturi mari de date, apelarea API-urilor lente ale terților, provizionarea resurselor externe, orice ar putea depăși o fereastră rezonabilă de răspuns HTTP.
* `shouldRunSynchronously: true` — hook-ul este executat **inline în timpul fluxului de instalare** (același executor ca pre-install). Cererea de instalare blochează până când handlerul se termină, iar dacă acesta aruncă o eroare, apelantul instalării primește un `POST_INSTALL_ERROR`. Fără reîncercări automate. **Folosiți acest mod pentru sarcini rapide, care trebuie să se finalizeze înainte de răspuns** — de exemplu, emiterea unei erori de validare către utilizator sau o configurare rapidă de care clientul va depinde imediat după ce apelul de instalare revine. Reține că migrarea metadatelor a fost deja aplicată până când rulează post-install, astfel încât un eșec în modul sincron **nu** anulează modificările de schemă — doar expune eroarea.
* Asigurați-vă că handlerul dvs. este idempotent. În modul asincron, coada poate reîncerca de până la trei ori; în oricare mod, hook-ul poate rula din nou la actualizări când `shouldRunOnVersionUpgrade: true`.
* Variabilele de mediu `APPLICATION_ID`, `APP_ACCESS_TOKEN` și `API_URL` sunt disponibile în interiorul handlerului (la fel ca în orice altă funcție logică), astfel încât puteți apela API-ul Twenty cu un token de acces al aplicației limitat la aplicația dvs.
* Este permisă o singură funcție de post-instalare per aplicație. Construirea manifestului va genera o eroare dacă este detectată mai mult de una.
* `universalIdentifier`, `shouldRunOnVersionUpgrade` și `shouldRunSynchronously` ale funcției sunt atașate automat la manifestul aplicației în câmpul `postInstallLogicFunction` în timpul build-ului — nu este nevoie să le referi în `defineApplication()`.
* Timpul de expirare implicit este setat la 300 de secunde (5 minute) pentru a permite sarcini de configurare mai lungi, cum ar fi popularea datelor.
* **Nu se execută în modul dev**: când o aplicație este înregistrată local (prin `yarn twenty dev`), serverul sare complet peste fluxul de instalare și sincronizează fișierele direct prin watcher-ul CLI — astfel încât post-install nu rulează niciodată în modul dev, indiferent de `shouldRunSynchronously`. Folosiți `yarn twenty exec --postInstall` pentru a-l declanșa manual într-un workspace care rulează.
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="Definește o funcție logică de pre-instalare (una per aplicație)">
O funcție de pre-instalare este o funcție logică ce rulează automat în timpul instalării, **înainte ca migrarea metadatelor workspace-ului să fie aplicată**. Are aceeași structură a payload-ului ca post-install (`InstallPayload`), dar este plasată mai devreme în fluxul de instalare, astfel încât poate pregăti starea de care depinde migrarea iminentă — utilizări tipice includ realizarea unui backup al datelor, validarea compatibilității cu noua schemă sau arhivarea înregistrărilor care urmează să fie restructurate sau eliminate.
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
const handler = async (payload: InstallPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
Puteți, de asemenea, să executați manual funcția de pre-instalare oricând folosind CLI:
```bash filename="Terminal"
yarn twenty exec --preInstall
```
Puncte cheie:
* Funcțiile de pre-instalare folosesc `definePreInstallLogicFunction()` — aceeași configurare specializată ca pentru post-install, doar că atașată la un alt punct din ciclul de viață.
* Atât handlerele de pre-install, cât și cele de post-install primesc același tip `InstallPayload`: `{ previousVersion?: string; newVersion: string }`. Importați-l o singură dată și reutilizați-l pentru ambele hook-uri.
* **Când rulează hook-ul**: poziționat chiar înainte de migrarea metadatelor workspace-ului (`synchronizeFromManifest`). Înainte de execuție, serverul rulează un "sync redus", pur aditiv, care înregistrează funcția de pre-instalare a versiunii **noi** în metadatele workspace-ului — nimic altceva nu este atins — și apoi o execută. Deoarece acest sync este doar aditiv, obiectele, câmpurile și datele versiunii precedente sunt încă intacte când rulează handlerul dvs.: puteți citi și face backup în siguranță stării pre-migrare.
* **Model de execuție**: pre-install este executat **sincron** și **blochează instalarea**. Dacă handlerul aruncă o eroare, instalarea este întreruptă înainte ca orice modificări de schemă să fie aplicate — workspace-ul rămâne la versiunea anterioară într-o stare consistentă. Acest lucru este intenționat: pre-install este ultima dvs. șansă de a refuza o actualizare riscantă.
* La fel ca la post-install, este permisă o singură funcție de pre-instalare per aplicație. Este atașată automat la manifestul aplicației sub `preInstallLogicFunction` în timpul build-ului.
* **Nu se execută în modul dev**: la fel ca post-install — fluxul de instalare este sărit complet pentru aplicațiile înregistrate local, astfel încât pre-install nu rulează niciodată sub `yarn twenty dev`. Folosiți `yarn twenty exec --preInstall` pentru a-l declanșa manual.
</Accordion>
<Accordion title="Pre-install vs post-install: când să folosești fiecare" description="Alegerea hook-ului de instalare potrivit">
Ambele hook-uri fac parte din același flux de instalare și primesc același `InstallPayload`. Diferența constă în **momentul** în care rulează în raport cu migrarea metadatelor workspace-ului, iar asta schimbă ce date pot atinge în siguranță.
```
┌─────────────────────────────────────────────────────────────┐
│ install flow │
│ │
│ upload package → [pre-install] → metadata migration → │
│ generate SDK → [post-install] │
│ │
│ old schema visible new schema visible │
└─────────────────────────────────────────────────────────────┘
```
Pre-install este întotdeauna **sincron** (blochează instalarea și o poate întrerupe). Post-install este **implicit asincron** — pus în coadă pe un worker cu reîncercări automate — dar poate opta pentru execuție sincronă cu `shouldRunSynchronously: true`. Consultați acordeonul `definePostInstallLogicFunction` de mai sus pentru când să folosiți fiecare mod.
**Folosiți `post-install` pentru orice are nevoie ca noua schemă să existe.** Acesta este cazul obișnuit:
* Popularea datelor implicite (crearea înregistrărilor inițiale, a vizualizărilor implicite, a conținutului demo) pentru obiectele și câmpurile adăugate recent.
* Înregistrarea webhook-urilor la servicii terțe, acum că aplicația are acreditările sale.
* Apelarea propriului tău API pentru a finaliza configurarea care depinde de metadatele sincronizate.
* Logică idempotentă de tipul "asigurați-vă că acest lucru există" care ar trebui să reconcilieze starea la fiecare actualizare — combină cu `shouldRunOnVersionUpgrade: true`.
Exemplu — populează o înregistrare `PostCard` implicită după instalare:
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { createClient } from './generated/client';
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
if (previousVersion) return; // fresh installs only
const client = createClient();
await client.postCard.create({
data: { title: 'Welcome to Postcard', content: 'Your first card!' },
});
};
export default definePostInstallLogicFunction({
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
name: 'post-install',
description: 'Seeds a welcome post card after install.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
handler,
});
```
**Folosiți `pre-install` atunci când o migrare altfel ar distruge sau ar corupe datele existente.** Deoarece pre-install rulează pe schema *anterioară* și eșecul său anulează actualizarea, acesta este locul potrivit pentru orice este riscant:
* **Crearea unui backup al datelor care urmează să fie eliminate sau restructurate** — de exemplu, elimini un câmp în v2 și trebuie să-i copiezi valorile într-un alt câmp sau să le exporți în stocare înainte de rularea migrării.
* **Arhivarea înregistrărilor pe care o nouă constrângere le-ar invalida** — de exemplu, un câmp devine `NOT NULL` și trebuie mai întâi să ștergi sau să corectezi rândurile cu valori nule.
* **Validarea compatibilității și refuzarea actualizării dacă datele curente nu pot fi migrate fără probleme** — aruncă din handler și instalarea se oprește fără ca modificări să fie aplicate. Aceasta este mai sigur decât să descoperi incompatibilitatea în mijlocul migrării.
* **Redenumirea sau schimbarea cheilor datelor** înaintea unei modificări de schemă care ar pierde asocierile.
Exemplu — arhivează înregistrări înainte de o migrare distructivă:
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { createClient } from './generated/client';
const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
// Only the 1.x → 2.x upgrade drops the legacy `notes` field.
if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
return;
}
const client = createClient();
const legacyRecords = await client.postCard.findMany({
where: { notes: { isNotNull: true } },
});
if (legacyRecords.length === 0) return;
// Copy legacy `notes` into the new `description` field before the migration
// drops the `notes` column. If this fails, the upgrade is aborted and the
// workspace stays on v1 with all data intact.
await Promise.all(
legacyRecords.map((record) =>
client.postCard.update({
where: { id: record.id },
data: { description: record.notes },
}),
),
);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Backs up legacy notes into description before the v2 migration.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
**Regulă practică:**
| Vrei să... | Folosiți |
| ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |
| Populați date implicite, configurați workspace-ul, înregistrați resurse externe | `post-install` |
| Rulați populări de durată sau apeluri către terți care nu ar trebui să blocheze răspunsul la instalare | `post-install` (implicit — `shouldRunSynchronously: false`, cu reîncercări ale workerului) |
| Rulați o configurare rapidă de care apelantul va depinde imediat după ce apelul de instalare revine | `post-install` cu `shouldRunSynchronously: true` |
| Citești sau faci backup datelor pe care migrarea iminentă le-ar pierde | `pre-install` |
| Respingeți o actualizare care ar corupe datele existente | `pre-install` (aruncă din handler) |
| Rulați o reconciliere la fiecare actualizare | `post-install` cu `shouldRunOnVersionUpgrade: true` |
| Faceți o configurare unică doar la prima instalare | `post-install` cu `shouldRunOnVersionUpgrade: false` (implicit) |
<Note>
Dacă aveți dubii, alegeți implicit **post-install**. Apelați la pre-install doar când migrarea în sine este distructivă și trebuie să interceptați starea anterioară înainte să dispară.
</Note>
</Accordion>
</AccordionGroup>
## Clienți API tipizați (twenty-client-sdk)
Pachetul `twenty-client-sdk` oferă doi clienți GraphQL tipați pentru a interacționa cu API-ul Twenty din funcțiile de logică și componentele Front.
| Client | Importați | Endpoint | Generat? |
| ------------------- | ---------------------------- | ------------------------------------------------------------------- | ---------------------------- |
| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — date ale spațiului de lucru (înregistrări, obiecte) | Da, în timpul dev/build |
| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — configurarea spațiului de lucru, încărcări de fișiere | Nu, este livrat preconstruit |
<AccordionGroup>
<Accordion title="CoreApiClient" description="Interogați și modificați datele spațiului de lucru (înregistrări, obiecte)">
`CoreApiClient` este clientul principal pentru interogarea și modificarea datelor din spațiul de lucru. Este generat din schema spațiului de lucru în timpul `yarn twenty dev` sau `yarn twenty build`, astfel încât este complet tipizat pentru a corespunde obiectelor și câmpurilor dvs.
```ts
import { CoreApiClient } from 'twenty-client-sdk/core';
const client = new CoreApiClient();
// Query records
const { companies } = await client.query({
companies: {
edges: {
node: {
id: true,
name: true,
domainName: {
primaryLinkLabel: true,
primaryLinkUrl: true,
},
},
},
},
});
// Create a record
const { createCompany } = await client.mutation({
createCompany: {
__args: {
data: {
name: 'Acme Corp',
},
},
id: true,
name: true,
},
});
```
Clientul folosește o sintaxă de tip selection-set: transmiteți `true` pentru a include un câmp, folosiți `__args` pentru argumente și imbricați obiecte pentru relații. Obțineți autocompletare și verificare a tipurilor complete, pe baza schemei spațiului dvs. de lucru.
<Note>
**CoreApiClient este generat în timpul dev/build.** Dacă îl utilizați fără a rula mai întâi `yarn twenty dev` sau `yarn twenty build`, va arunca o eroare. Generarea are loc automat — CLI inspectează schema GraphQL a spațiului dvs. de lucru și generează un client tipizat folosind `@genql/cli`.
</Note>
#### Folosirea CoreSchema pentru adnotări de tip
`CoreSchema` oferă tipuri TypeScript care corespund obiectelor din spațiul dvs. de lucru — utile pentru tiparea stării componentelor sau a parametrilor funcțiilor:
```ts
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
import { useState } from 'react';
const [company, setCompany] = useState<
Pick<CoreSchema.Company, 'id' | 'name'> | undefined
>(undefined);
const client = new CoreApiClient();
const result = await client.query({
company: {
__args: { filter: { position: { eq: 1 } } },
id: true,
name: true,
},
});
setCompany(result.company);
```
</Accordion>
<Accordion title="MetadataApiClient" description="Configurația spațiului de lucru, aplicații și încărcări de fișiere">
`MetadataApiClient` este livrat preconstruit împreună cu SDK-ul (nu este necesară generarea). Interoghează endpointul `/metadata` pentru configurarea spațiului de lucru, aplicații și încărcări de fișiere.
```ts
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
const metadataClient = new MetadataApiClient();
// List first 10 objects in the workspace
const { objects } = await metadataClient.query({
objects: {
edges: {
node: {
id: true,
nameSingular: true,
namePlural: true,
labelSingular: true,
isCustom: true,
},
},
__args: {
filter: {},
paging: { first: 10 },
},
},
});
```
#### Încărcarea fișierelor
`MetadataApiClient` include o metodă `uploadFile` pentru atașarea fișierelor la câmpuri de tip fișier:
```ts
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import * as fs from 'fs';
const metadataClient = new MetadataApiClient();
const fileBuffer = fs.readFileSync('./invoice.pdf');
const uploadedFile = await metadataClient.uploadFile(
fileBuffer, // file contents as a Buffer
'invoice.pdf', // filename
'application/pdf', // MIME type
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier
);
console.log(uploadedFile);
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
```
| Parametru | Tip | Descriere |
| ---------------------------------- | -------- | ------------------------------------------------------------------- |
| `fileBuffer` | `Buffer` | Conținutul brut al fișierului |
| `filename` | `string` | Numele fișierului (folosit pentru stocare și afișare) |
| `contentType` | `string` | Tipul MIME (implicit `application/octet-stream` dacă este omis) |
| `fieldMetadataUniversalIdentifier` | `string` | `universalIdentifier` al câmpului de tip fișier de pe obiectul dvs. |
Puncte cheie:
* Folosește `universalIdentifier` al câmpului (nu ID-ul specific spațiului de lucru), astfel încât codul dvs. de încărcare funcționează în orice spațiu de lucru în care aplicația dvs. este instalată.
* `url` returnat este un URL semnat pe care îl puteți folosi pentru a accesa fișierul încărcat.
</Accordion>
</AccordionGroup>
<Note>
Când codul dvs. rulează pe Twenty (funcții de logică sau componente Front), platforma injectează acreditările ca variabile de mediu:
* `TWENTY_API_URL` — URL-ul de bază al API-ului Twenty
* `TWENTY_APP_ACCESS_TOKEN` — Cheie cu durată scurtă, limitată la rolul implicit de funcție al aplicației
Nu trebuie să le transmiteți clienților — aceștia citesc automat din `process.env`. Permisiunile cheii API sunt determinate de rolul referențiat în `defaultRoleUniversalIdentifier` din `application-config.ts`.
</Note>
@@ -1,9 +1,12 @@
---
title: Publicare
icon: încarcă
description: Distribuie aplicația ta Twenty în marketplace sau implementeaz-o intern.
---
<Warning>
Aplicațiile sunt în prezent în testare alfa. Caracteristica funcționează, dar este încă în dezvoltare.
</Warning>
## Prezentare generală
După ce aplicația ta este [construită și testată local](/l/ro/developers/extend/apps/building), ai două căi pentru distribuire:
@@ -63,104 +66,14 @@ Linkul de partajare folosește URL-ul de bază al serverului (fără niciun subd
### Gestionarea versiunilor
Când actualizezi o aplicație tarball deja implementată, serverul solicită ca `version` din `package.json` să fie **strict mai mare** (conform ordonării [semver](https://semver.org)) decât versiunea implementată în prezent. Redeployarea aceleiași versiuni sau trimiterea uneia inferioare este respinsă înainte ca tarball-ul să fie stocat — vei vedea o eroare `VERSION_ALREADY_EXISTS` de la CLI.
Pentru a lansa o actualizare:
1. Incrementați câmpul `version` din `package.json` (de ex. `1.2.3` → `1.2.4`, `1.3.0` sau `2.0.0`)
1. Actualizează câmpul `version` din `package.json`
2. Rulează `yarn twenty deploy` (sau `yarn twenty deploy --remote production`)
3. Spațiile de lucru care au aplicația instalată vor vedea actualizarea disponibilă în setările lor
<Note>
Etichetele de pre-lansare funcționează conform așteptărilor: incrementarea de la `1.0.0-rc.1` la `1.0.0-rc.2` este permisă, iar o lansare finală precum `1.0.0` este recunoscută corect ca fiind mai mare decât `1.0.0-rc.5`. Versiunea din `package.json` trebuie să fie ea însăși un șir semver valid.
</Note>
{/* TODO: add screenshot of the Upgrade button */}
### Compatibilitatea versiunii serverului
Dacă aplicația ta folosește o funcționalitate introdusă într-o anumită versiune de server Twenty (de exemplu, furnizori OAuth adăugați în v2.3.0), ar trebui să declari versiunea minimă de server necesară aplicației folosind câmpul `engines.twenty` din `package.json`:
```json filename="package.json"
{
"name": "twenty-my-app",
"version": "1.0.0",
"engines": {
"node": "^24.5.0",
"twenty": ">=2.3.0"
}
}
```
Valoarea este un [interval semver](https://github.com/npm/node-semver#ranges) standard. Tipare comune:
| Interval | Semnificație |
| ---------------------------------- | ------------------------------------------------------ |
| `>=2.3.0` | Orice server de la 2.3.0 încolo |
| `>=2.3.0 \<3.0.0` | 2.3.0 sau ulterior, dar sub următoarea versiune majoră |
| `^2.3.0` | La fel ca `>=2.3.0 \<3.0.0` |
**Ce se întâmplă în timpul implementării și instalării:**
* Dacă `engines.twenty` este setat și versiunea serverului țintă nu respectă intervalul, implementarea (încărcarea arhivei tarball) sau instalarea este respinsă cu eroarea `SERVER_VERSION_INCOMPATIBLE` și cu un mesaj care indică atât intervalul necesar, cât și versiunea efectivă a serverului.
* Dacă `engines.twenty` nu este setat, aplicația este acceptată pe orice versiune de server (retrocompatibilă cu aplicațiile existente).
* Dacă serverul nu are nicio `APP_VERSION` configurată, verificarea este omisă.
<Note>
Serverul este verificarea autoritativă — validează `engines.twenty` atât la încărcarea arhivei tarball, cât și la instalarea în spațiul de lucru. Dacă implementezi un tarball în afara fluxului standard sau instalezi din marketplace, serverul impune în continuare compatibilitatea.
</Note>
## CI/CD automatizat (fluxuri de lucru preconfigurate)
Aplicațiile generate cu `create-twenty-app` vin, gata de utilizare, cu două fluxuri de lucru GitHub Actions, în `.github/workflows/`. Acestea sunt gata să ruleze imediat ce faci push al repozitoriului pe GitHub — nu este necesară nicio configurare suplimentară pentru CI, iar CD necesită doar un singur secret.
### CI — `ci.yml`
Rulează testele de integrare la fiecare push pe `main` și la fiecare pull request.
**Ce face:**
1. Preia codul sursă al aplicației.
2. Pornește o instanță de test Twenty izolată folosind acțiunea compozită `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main` (echivalentul din CI al `yarn twenty server start --test`).
3. Activează Corepack, configurează Node.js pe baza fișierului `.nvmrc` și instalează dependențele cu `yarn install --immutable`.
4. Rulează `yarn test`, transmitând `TWENTY_API_URL` și `TWENTY_API_KEY` din instanța pornită, astfel încât testele să poată comunica cu un server real.
**Opțiuni de configurare:**
* `TWENTY_VERSION` (variabilă de mediu, implicit `latest`) — fixează versiunea serverului Twenty folosită în CI editând acest parametru în `ci.yml`.
* Concurența este grupată după `github.ref` și anulează execuțiile în desfășurare la noile push-uri.
Nu sunt necesare secrete — instanța de test este efemeră și există doar pe durata jobului.
### CD — `cd.yml`
Implementează aplicația pe un server Twenty configurat la fiecare push pe `main` și, opțional, dintr-un pull request când se aplică eticheta `deploy`.
**Ce face:**
1. Preia head-ul PR-ului (pentru PR-urile etichetate) sau commitul împins.
2. Rulează `twentyhq/twenty/.github/actions/deploy-twenty-app@main` — echivalentul din CI al `yarn twenty deploy`.
3. Rulează `twentyhq/twenty/.github/actions/install-twenty-app@main` astfel încât versiunea nou implementată să fie instalată în spațiul de lucru țintă.
**Configurare necesară:**
| Setare | Unde | Scop |
| ----------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `TWENTY_DEPLOY_URL` | `env` în `cd.yml` (implicit `http://localhost:3000`) | Serverul Twenty la care se face implementarea. Modifică-l la URL-ul real al serverului înainte de prima utilizare. |
| `TWENTY_DEPLOY_API_KEY` | GitHub repo **Settings → Secrets and variables → Actions** | Cheie API cu permisiune de implementare pe serverul țintă. |
<Note>
Valoarea implicită a `TWENTY_DEPLOY_URL`, `http://localhost:3000`, este un placeholder — nu va putea accesa nimic dintr-un runner găzduit de GitHub. Actualizează-l la URL-ul public al serverului tău (sau folosește un runner self-hosted cu acces la rețea) înainte de a activa CD.
</Note>
**Declanșarea unei implementări de previzualizare dintr-un PR:**
Adaugă eticheta `deploy` la un pull request. Condiția `if:` din `cd.yml` va rula jobul pentru acel PR folosind commitul head al PR-ului, permițându-ți să validezi o modificare pe serverul țintă înainte de a face merge.
### Fixarea acțiunilor reutilizabile
Ambele fluxuri de lucru fac referire la acțiuni reutilizabile la `@main`, astfel încât actualizările acțiunilor din repo-ul `twentyhq/twenty` sunt preluate automat. Dacă dorești builduri deterministe, înlocuiește `@main` cu un SHA de commit sau cu un tag de release pe fiecare linie `uses:`.
## Publicarea pe npm
Publicarea pe npm face ca aplicația ta să poată fi descoperită în marketplace-ul Twenty. Orice spațiu de lucru Twenty poate răsfoi, instala și actualiza aplicațiile din marketplace direct din interfață.
@@ -168,7 +81,7 @@ Publicarea pe npm face ca aplicația ta să poată fi descoperită în marketpla
### Cerințe
* Un cont [npm](https://www.npmjs.com)
* Cuvântul cheie `twenty-app` din array-ul `keywords` al fișierului `package.json` (adaugă-l manual — nu este inclus în mod implicit în șablonul `create-twenty-app`)
* Cuvântul cheie `twenty-app` din array-ul `keywords` al fișierului `package.json` (deja inclus când inițializezi proiectul cu `create-twenty-app`)
```json filename="package.json"
{
@@ -198,14 +111,6 @@ export default defineApplication({
Vezi [acordeonul defineApplication](/l/ro/developers/extend/apps/building#defineentity-functions) din pagina Building Apps pentru lista completă de câmpuri ale marketplace-ului (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl`, etc.).
#### Dimensiuni recomandate pentru capturi de ecran
marketplace-ul redă `screenshots` într-un container fix cu raport `8:5` (de exemplu, `1600×1000 px`).
<Note>
Capturile de ecran cu orice raport de aspect sunt afișate integral și nu sunt niciodată decupate, însă orice este semnificativ mai înalt sau mai îngust decât `8:5` va afișa benzi goale pe laterale.
</Note>
### Publicare
```bash filename="Terminal"
@@ -225,9 +130,9 @@ Serverul Twenty sincronizează catalogul marketplace-ului din registrul npm **la
Poți declanșa sincronizarea imediat, în loc să aștepți:
```bash filename="Terminal"
yarn twenty server catalog-sync
yarn twenty catalog-sync
# To target a specific remote:
# yarn twenty server catalog-sync --remote production
# yarn twenty catalog-sync --remote production
```
Metadatele afișate în marketplace provin din configurația `defineApplication()` — câmpuri precum `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl` și `termsUrl`.
@@ -284,12 +189,3 @@ Poți instala aplicații și din linia de comandă:
```bash filename="Terminal"
yarn twenty install
```
<Note>
Serverul impune versionarea semver la instalare, reflectând regulile de la deploy:
* Instalarea aceleiași versiuni care este deja instalată în workspace-ul tău este respinsă cu o eroare `APP_ALREADY_INSTALLED`.
* Instalarea unei versiuni mai mici decât cea instalată în prezent este respinsă cu o eroare `CANNOT_DOWNGRADE_APPLICATION`.
Pentru a instala o versiune mai nouă, fă mai întâi deploy sau public-o, apoi rulează din nou `yarn twenty install`.
</Note>
@@ -1,69 +0,0 @@
---
title: Abilități și agenți
description: Definiți abilități și agenți AI pentru aplicația dvs.
icon: robot
---
<Warning>
Aptitudinile și agenții sunt în prezent în testare alfa. Caracteristica funcționează, dar este încă în dezvoltare.
</Warning>
Aplicațiile pot defini capabilități AI care există în interiorul spațiului de lucru — instrucțiuni reutilizabile pentru abilități și agenți cu prompturi de sistem personalizate.
<AccordionGroup>
<Accordion title="defineSkill" description="Definiți abilități pentru agentul AI">
Abilitățile definesc instrucțiuni și capabilități reutilizabile pe care agenții AI le pot folosi în spațiul dvs. de lucru. Folosiți `defineSkill()` pentru a defini abilități cu validare încorporată:
```ts src/skills/example-skill.ts
import { defineSkill } from 'twenty-sdk/define';
export default defineSkill({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'sales-outreach',
label: 'Sales Outreach',
description: 'Guides the AI agent through a structured sales outreach process',
icon: 'IconBrain',
content: `You are a sales outreach assistant. When reaching out to a prospect:
1. Research the company and recent news
2. Identify the prospect's role and likely pain points
3. Draft a personalized message referencing specific details
4. Keep the tone professional but conversational`,
});
```
Puncte cheie:
* `name` este un șir identificator unic pentru abilitate (se recomandă kebab-case).
* `label` este numele lizibil afișat în interfața cu utilizatorul (UI).
* `content` conține instrucțiunile abilității — acesta este textul pe care agentul AI îl folosește.
* `icon` (opțional) setează pictograma afișată în UI.
* `description` (opțional) oferă context suplimentar despre scopul abilității.
</Accordion>
<Accordion title="defineAgent" description="Definiți agenți AI cu prompturi personalizate">
Agenții sunt asistenți AI care există în interiorul spațiului dvs. de lucru. Utilizați `defineAgent()` pentru a crea agenți cu un prompt de sistem personalizat:
```ts src/agents/example-agent.ts
import { defineAgent } from 'twenty-sdk/define';
export default defineAgent({
universalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
name: 'sales-assistant',
label: 'Sales Assistant',
description: 'Helps the sales team draft outreach emails and research prospects',
icon: 'IconRobot',
prompt: 'You are a helpful sales assistant. Help users with their questions and tasks.',
});
```
Puncte cheie:
* `name` este un șir identificator unic pentru agent (se recomandă kebab-case).
* `label` este numele de afișare din interfața cu utilizatorul (UI).
* `prompt` conține promptul de sistem — acesta este textul de instrucțiuni care definește comportamentul agentului.
* `description` (opțional) oferă context suplimentar despre scopul agentului.
* `icon` (opțional) setează pictograma afișată în UI.
* `modelId` (opțional) suprascrie modelul AI implicit utilizat de agent.
</Accordion>
</AccordionGroup>
@@ -1,189 +0,0 @@
---
title: OAuth
icon: cheie
description: Fluxul codului de autorizare cu PKCE și acreditări de client pentru acces server-la-server.
---
Twenty implementează OAuth 2.0 cu cod de autorizare + PKCE pentru aplicațiile orientate către utilizatori și acreditări de client pentru acces server-la-server. Clienții sunt înregistrați dinamic prin [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591) — fără configurare manuală într-un panou de control.
## Când să folosiți OAuth
| Scenariu | Metodă de autentificare |
| ----------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| Scripturi interne, automatizare | [Cheie API](/l/ro/developers/extend/api#authentication) |
| Aplicație externă care acționează în numele unui utilizator | **OAuth — Cod de autorizare** |
| Server-la-server, fără context de utilizator | **OAuth — Acreditări de client** |
| Aplicație Twenty cu extensii UI | [Aplicații](/l/ro/developers/extend/apps/getting-started) (OAuth este gestionat automat) |
## Înregistrați un client
Twenty acceptă **înregistrarea dinamică a clienților** conform [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591). Nu este necesară configurare manuală — înregistrați programatic:
```bash
POST /oauth/register
Content-Type: application/json
{
"client_name": "My Integration",
"redirect_uris": ["https://myapp.com/callback"],
"grant_types": ["authorization_code"],
"token_endpoint_auth_method": "client_secret_post"
}
```
**Răspuns:**
```json
{
"client_id": "abc123",
"client_secret": "secret456",
"client_name": "My Integration",
"redirect_uris": ["https://myapp.com/callback"]
}
```
<Warning>
Stocați `client_secret` în siguranță — nu poate fi recuperat ulterior.
</Warning>
## Domenii de aplicare
| Domeniu de aplicare | Acces |
| ------------------- | -------------------------------------------------------------- |
| `api` | Acces complet de citire/scriere la API-urile Core și Metadata |
| `profile` | Citește informațiile de profil ale utilizatorului autentificat |
Solicitați domeniile de aplicare ca un șir separat prin spații: `scope=api profile`
## Fluxul codului de autorizare
Folosiți acest flux când aplicația dvs. acționează în numele unui utilizator Twenty.
### 1. Redirecționați utilizatorul pentru autorizare
```
GET /oauth/authorize?
client_id=YOUR_CLIENT_ID&
response_type=code&
redirect_uri=https://myapp.com/callback&
scope=api&
state=random_state_value&
code_challenge=CHALLENGE&
code_challenge_method=S256
```
| Parametru | Obligatoriu | Descriere |
| ----------------------- | ----------- | --------------------------------------------------------------------- |
| `client_id` | Da | ID-ul clientului înregistrat |
| `response_type` | Da | Trebuie să fie `code` |
| `redirect_uri` | Da | Trebuie să corespundă unei adrese URI de redirecționare înregistrate |
| `scope` | Nu | Domenii de aplicare separate prin spațiu (implicit `api`) |
| `state` | Recomandat | Șir aleatoriu pentru a preveni atacurile CSRF |
| `code_challenge` | Recomandat | Provocare PKCE (hash SHA-256 al verificatorului, codificat base64url) |
| `code_challenge_method` | Recomandat | Trebuie să fie `S256` când se folosește PKCE |
Utilizatorul vede un ecran de consimțământ și aprobă sau refuză accesul.
### 2. Gestionați callback-ul
După autorizare, Twenty redirecționează înapoi către `redirect_uri`-ul dvs.:
```
https://myapp.com/callback?code=AUTH_CODE&state=random_state_value
```
Verificați că `state` corespunde cu ceea ce ați trimis.
### 3. Schimbați codul pe tokenuri
```bash
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=AUTH_CODE&
redirect_uri=https://myapp.com/callback&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
code_verifier=YOUR_PKCE_VERIFIER
```
**Răspuns:**
```json
{
"access_token": "eyJhbG...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBh..."
}
```
### 4. Utilizați tokenul de acces
```bash
GET /rest/companies
Authorization: Bearer ACCESS_TOKEN
```
### 5. Reîmprospătați la expirare
```bash
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&
refresh_token=YOUR_REFRESH_TOKEN&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET
```
## Fluxul cu acreditări de client
Pentru integrări server-la-server fără interacțiune cu utilizatorul:
```bash
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
scope=api
```
Tokenul returnat are acces la nivel de spațiu de lucru, neasociat unui utilizator specific.
## Descoperirea serverului
Twenty publică configurația sa OAuth la un endpoint standard de descoperire:
```
GET /.well-known/oauth-authorization-server
```
Acesta returnează toate endpoint-urile, tipurile de grant acceptate, domeniile de aplicare și capabilitățile — util pentru construirea de clienți OAuth generici.
## Rezumatul endpoint-urilor API
| Endpoint | Scop |
| ----------------------------------------- | -------------------------------------- |
| `/.well-known/oauth-authorization-server` | Descoperirea metadatelor serverului |
| `/oauth/register` | Înregistrare dinamică a clientului |
| `/oauth/authorize` | Autorizarea utilizatorului |
| `/oauth/token` | Schimb și reîmprospătare a tokenurilor |
| Mediu | URL de bază |
| -------------------- | ------------------------ |
| **Cloud** | `https://api.twenty.com` |
| **Găzduire proprie** | `https://{your-domain}` |
## OAuth vs Chei API
| | Chei API | OAuth |
| -------------------------------- | ----------------------------------- | --------------------------------------------- |
| **Configurare** | Generați în Setări | Înregistrați un client, implementați fluxul |
| **Contextul utilizatorului** | Niciunul (nivel de spațiu de lucru) | Permisiunile utilizatorului specific |
| **Cel mai potrivit pentru** | Scripturi, instrumente interne | Aplicații externe, integrări multi-utilizator |
| **Rotirea tokenurilor** | Manual | Automată prin tokenuri de reîmprospătare |
| **Acces pe domenii de aplicare** | Acces API complet | Granular, prin domenii de aplicare |
@@ -1,12 +1,11 @@
---
title: Webhooks
icon: satellite-dish
description: Primește notificări când înregistrările se modifică — HTTP POST către endpoint-ul tău la fiecare creare, actualizare sau ștergere.
description: Primiți notificări în timp real atunci când au loc evenimente în CRM-ul dvs.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
Twenty trimite o solicitare HTTP POST către URL-ul tău ori de câte ori o înregistrare este creată, actualizată sau ștearsă. Toate tipurile de obiecte sunt acoperite, inclusiv obiectele personalizate.
Webhook-urile trimit date către sistemele dvs. în timp real atunci când au loc evenimente în Twenty — nu este necesară interogarea periodică. Folosiți-le pentru a menține sistemele externe sincronizate, a declanșa automatizări sau a trimite alerte.
## Creați un webhook
@@ -1,28 +1,23 @@
---
title: Dezvoltatori
description: Creați aplicații, utilizați API-ul, autogăzduiți sau contribuiți la baza de cod.
title: Începeți
description: Bun venit la Documentația Twenty pentru dezvoltatori, resursa dvs. pentru extindere, autogăzduire și contribuții la Twenty.
---
import { CardTitle } from "/snippets/card-title.mdx"
<CardGroup cols={3}>
<Card href="/l/ro/developers/extend/apps/getting-started" img="/images/user-guide/halftone/dev-apps.png">
<CardTitle>Aplicații</CardTitle>
Extindeți Twenty cu obiecte personalizate, logică pe partea de server, componente UI și agenți AI — toate sub formă de pachete TypeScript.
<Card href="/l/ro/developers/extend/extend" img="/images/user-guide/integrations/plug.png">
<CardTitle>Extindeți</CardTitle>
Creați integrări cu API-uri, webhook-uri și aplicații personalizate.
</Card>
<Card href="/l/ro/developers/extend/api" img="/images/user-guide/halftone/dev-api.png">
<CardTitle>API</CardTitle>
API-uri REST și GraphQL, webhook-uri și OAuth.
</Card>
<Card href="/l/ro/developers/self-host/capabilities/docker-compose" img="/images/user-guide/halftone/dev-self-host.png">
<Card href="/l/ro/developers/self-host/self-host" img="/images/user-guide/what-is-twenty/20.png">
<CardTitle>Autogăzduire</CardTitle>
Rulați Twenty pe propria infrastructură.
Implementați și gestionați Twenty pe propria infrastructură.
</Card>
<Card href="/l/ro/developers/contribute/capabilities/local-setup" img="/images/user-guide/halftone/dev-contribute.png">
<Card href="/l/ro/developers/contribute/contribute" img="/images/user-guide/github/github-header.png">
<CardTitle>Contribuiți</CardTitle>
Configurați monorepo-ul local și trimiteți PR-uri.
Alăturați-vă comunității noastre cu sursă deschisă și contribuiți la Twenty.
</Card>
</CardGroup>
@@ -1,6 +1,5 @@
---
title: Alte metode
icon: cloud
---
<Warning>
@@ -1,6 +1,5 @@
---
title: Docker Compose
icon: docker
title: 1-Click cu Docker Compose
---
<Warning>
@@ -1,6 +1,5 @@
---
title: Configurare
icon: gear
---
# Gestionarea configurației
@@ -1,6 +1,5 @@
---
title: Depanare
icon: wrench
---
## Depanare
@@ -1,90 +1,381 @@
---
title: Ghid de actualizare
icon: arrow-up-right-dots
---
## Linii directoare generale
**Faceți întotdeauna o copie de rezervă a bazei de date înainte de a începe procesul de actualizare** rulând:
**Asigurați-vă întotdeauna că realizați un backup al bazei de date înainte de a începe procesul de actualizare** utilizând comanda `docker exec -it {db_container_name_or_id} pg_dumpall -U {postgres_user} > databases_backup.sql`.
Pentru a restaura backup-ul, executați comanda `cat databases_backup.sql | docker exec -i {db_container_name_or_id} psql -U {postgres_user}`.
Dacă ați utilizat Docker Compose, urmați acești pași:
1. Într-un terminal, pe găzduitorul unde rulează Twenty, opriți Twenty: `docker compose down`.
2. Actualizați versiunea modificând valoarea `TAG` din fișierul .env în apropierea docker-compose-ului. ( Vă recomandăm să utilizați o versiune de tip `major.minor`, cum ar fi `v0.53` )
3. Repuneți Twenty în funcțiune cu comanda `docker compose up -d`.
Dacă doriți să actualizați instanța dvs. cu câteva versiuni, de exemplu de la v0.33.0 la v0.35.0, trebuie să actualizați instanța dvs. secvențial, în acest exemplu de la v0.33.0 la v0.34.0, apoi de la v0.34.0 la v0.35.0.
**Asigurați-vă că după fiecare actualizare de versiune aveți un backup necorupt.**
## Pași de actualizare specifici versiunii
## v1.0
Salut Twenty v1.0! 🎉
## v0.60
### Îmbunătățiri de performanță
Toate interacțiunile cu API-ul de metadata au fost optimizate pentru o performanță mai bună, în special pentru manipularea metadata-ului obiectelor și operațiile de creare a spațiilor de lucru.
Am refăcut strategia de caching pentru a prioritiza accesările din cache în detrimentul interogărilor către baza de date, îmbunătățind semnificativ performanța operațiunilor API-ului de metadata.
Dacă întâmpinați probleme de runtime după actualizare, s-ar putea să fie nevoie să goliți cache-ul pentru a vă asigura că este sincronizat cu ultimele schimbări. Rulați această comandă în containerul dvs. twenty-server:
```bash
docker exec -it {db_container_name_or_id} pg_dumpall -U {postgres_user} > databases_backup.sql
yarn command:prod cache:flush
```
Pentru a restaura din copia de rezervă:
### v0.55
Actualizați instanța Twenty pentru a utiliza imaginea v0.55
Nu este necesar să mai rulați nici o comandă, noua imagine se va ocupa automat de rularea tuturor migrațiilor necesare.
### Eroare `Utilizatorul nu are permisiune`
Dacă întâmpinați erori de autorizare la majoritatea cererilor după actualizare, s-ar putea să fie nevoie să goliți cache-ul pentru a recalcula permisiunile actualizate.
În containerul dvs. `twenty-server`, rulați:
```bash
cat databases_backup.sql | docker exec -i {db_container_name_or_id} psql -U {postgres_user}
yarn command:prod cache:flush
```
Dacă utilizați Docker Compose, urmați acești pași:
Această problemă este specifică versiunii Twenty și nu ar trebui să fie necesară pentru actualizările viitoare.
1. Opriți Twenty: `docker compose down`
2. Modificați valoarea `TAG` în fișierul `.env` de lângă `docker-compose.yml`
3. Porniți Twenty: `docker compose up -d`
### v0.54
Serverul rulează automat toate migrațiile de actualizare necesare la pornire. Nu este necesară nicio comandă manuală.
Începând cu versiunea `0.53`, nu sunt necesare acțiuni manuale.
## Actualizări între versiuni (v1.22+)
#### Depășirea schemelor de metadata
Începând cu **v1.22**, Twenty acceptă actualizări între versiuni. Puteți trece direct de la orice versiune acceptată la cea mai recentă versiune, fără a parcurge fiecare versiune intermediară.
Am combinat schema `metadata` cu cea `core` pentru a simplifica extragerea datelor din `TypeORM`.
Am îmbinat pasul de comandă `migrate` în cadrul comenzii `upgrade`. Nu recomandăm rularea manuală a comenzii `migrate` în oricare dintre containerele dvs. server/worker.
De exemplu, actualizarea de la v1.22 direct la v2.0 este pe deplin acceptată.
### Începând cu v0.53
## Verificarea stării actualizării
Începând cu `0.53`, actualizarea se face programatic în cadrul `DockerFile`, ceea ce înseamnă că de acum nu ar trebui să mai fie necesar să rulați manual nici o comandă.
Comanda `upgrade:status` vă permite să inspectați starea curentă a instanței și a migrațiilor spațiilor de lucru. Este utilă pentru depanarea problemelor de actualizare sau când trimiteți o solicitare de asistență.
Asigurați-vă că vă actualizați instanța secvențial, fără a sări peste nicio versiune majoră (de exemplu, `0.43.3` to `0.44.0` este permis, dar `0.43.1` to `0.45.0` nu este), altfel ar putea duce la desincronizare a versiunii spațiului de lucru, care ar putea rezulta în erori de runtime și funcționalități lipsă.
Rulați comanda din containerul serverului:
Pentru a verifica dacă un spațiu de lucru a fost migrat corect, puteți verifica versiunea acestuia în baza de date în tabelul `core.workspace`.
```bash
docker exec -it {server_container_name_or_id} yarn command:prod upgrade:status
Ar trebui să fie mereu în intervalul versiunii Twenty curente `major.minor`, puteți vizualiza versiunea instanței dvs. în panoul de administrare (la `/settings/admin-panel`, accesibil dacă utilizatorul dvs. are proprietatea `canAccessFullAdminPanel` setată la adevărat în baza de date) sau executând `echo $APP_VERSION` în containerul dvs. `twenty-server`.
Pentru a remedia o desincronizare a versiunii spațiului de lucru, va trebui să actualizați din versiunea corespunzătoare Twenty urmând ghidul de actualizare relevant secvențial și așa mai departe până când ajungeți la versiunea dorită.
#### Eliminarea `auditLog`
Am eliminat obiectul standard auditLog, ceea ce înseamnă că dimensiunea backup-ului dvs. ar putea fi redusă semnificativ după această migrație.
### v0.51 la v0.52
Actualizați instanța Twenty pentru a utiliza imaginea v0.52
```
yarn database:migrate:prod
yarn command:prod upgrade
```
Exemplu de rezultat:
#### Am un spațiu de lucru blocat în versiunea între `0.52.0` și `0.52.6`.
```sh
APP_VERSION: v1.23.0
Din păcate, `0.52.0` și `0.52.6` au fost complet eliminate din dockerHub.
Va trebui să actualizați manual versiunea spațiului de lucru la `0.51.0` în baza de date și să actualizați utilizând versiunea twenty `0.52.11` urmând ghidul de actualizare de mai sus.
Instance
Inferred version: 1.23.0
Latest command: 1.23.0_DropWorkspaceVersionColumnFastInstanceCommand_1785000000000
Status: Up to date
Executed by: v1.23.0
At: 2026-04-16T11:43:58.823Z
### v0.50 la v0.51
Workspace
Apple (20202020-1c25-4d02-bf25-6aeccf7ea419)
Inferred version: 1.23.0
Latest command: 1.23.0_UpdateGlobalObjectContextCommandMenuItemsCommand_1780000005000
Status: Up to date
Executed by: v1.23.0
At: 2026-04-16T11:44:09.361Z
Actualizați instanța Twenty pentru a utiliza imaginea v0.51
Summary
Instance: Up to date
Workspaces: 1 up to date, 0 behind, 0 failed (1 total)
```
yarn database:migrate:prod
yarn command:prod upgrade
```
### Opțiuni
### v0.44.0 la v0.50.0
| Opțiune | Descriere |
| ------------------------- | --------------------------------------------------------------------------------------- |
| `-w, --workspace-id <id>` | Filtrați după un spațiu de lucru specific. Poate fi specificată de mai multe ori. |
| `-f, --failed-only` | Ascunde spațiile de lucru la zi, afișând doar pe cele rămase în urmă și pe cele eșuate. |
Actualizați instanța Twenty pentru a utiliza imaginea v0.50.0
## Depanare
Dacă actualizarea eșuează pe unele spații de lucru, serverul nu va trece de pasul care a eșuat. Repornirea serverului (`docker compose up -d`) va reîncerca actualizarea de unde a rămas.
Pentru a identifica rapid problemele, rulați:
```bash
docker exec -it {server_container_name_or_id} yarn command:prod upgrade:status --failed-only
```
yarn database:migrate:prod
yarn command:prod upgrade
```
Aceasta afișează doar spațiile de lucru care sunt rămase în urmă sau au eșuat, împreună cu mesajul de eroare pentru fiecare eșec.
#### Mutarea docker-compose.yml
## Înainte de v1.22
Această versiune include o mutație `docker-compose.yml` pentru a oferi serviciului `worker` acces la volumul `server-local-data`.
Vă rugăm să actualizați docker-compose.yml-ul dvs. local cu [v0.50.0 docker-compose.yml](https://github.com/twentyhq/twenty/blob/v0.50.0/packages/twenty-docker/docker-compose.yml)
Dacă instanța dvs. este mai veche decât v1.22, trebuie să actualizați incremental prin fiecare versiune majoră etichetată (de la v1.6 la v1.7, apoi de la v1.7 la v1.8 și așa mai departe) până ajungeți la v1.22. De acolo, puteți sări direct la cea mai recentă versiune.
### v0.43.0 la v0.44.0
Actualizați instanța Twenty pentru a utiliza imaginea v0.44.0
```
yarn database:migrate:prod
yarn command:prod upgrade
```
### v0.42.0 la v0.43.0
Actualizați instanța Twenty pentru a utiliza imaginea v0.43.0
```
yarn database:migrate:prod
yarn command:prod upgrade
```
În această versiune, am trecut și la imaginea postgres:16 în docker-compose.yml.
#### (Opțiunea 1) Migrarea bazei de date
Păstrarea imaginii existente postgres-spilo este adecvată, însă va trebui să congelați versiunea în docker-compose.yml la 0.43.0.
#### (Opțiunea 2) Migrarea bazei de date
Dacă doriți să migrați baza de date la noua imagine postgres:16, urmați acești pași:
1. Copieți baza de date din vechiul container postgres-spilo
```
docker exec -it twenty-db-1 sh
pg_dump -U {YOUR_POSTGRES_USER} -d {YOUR_POSTGRES_DB} > databases_backup.sql
exit
docker cp twenty-db-1:/home/postgres/databases_backup.sql .
```
Asigurați-vă că fișierul de backup nu este gol.
2. Actualizați docker-compose.yml-ul pentru a utiliza imaginea postgres:16 din fișierul [docker-compose.yml](https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/docker-compose.yml).
3. Restaurați baza de date în noul container postgres:16
```
docker cp databases_backup.sql twenty-db-1:/databases_backup.sql
docker exec -it twenty-db-1 sh
psql -U {YOUR_POSTGRES_USER} -d {YOUR_POSTGRES_DB} -f databases_backup.sql
exit
```
### v0.41.0 la v0.42.0
Actualizați instanța Twenty pentru a utiliza imaginea v0.42.0
```
yarn database:migrate:prod
yarn command:prod upgrade-0.42
```
**Variabile de mediu**
* Eliminat: `FRONT_PORT`, `FRONT_PROTOCOL`, `FRONT_DOMAIN`, `PORT`
* Adăugat: `FRONTEND_URL`, `NODE_PORT`, `MAX_NUMBER_OF_WORKSPACES_DELETED_PER_EXECUTION`, `MESSAGING_PROVIDER_MICROSOFT_ENABLED`, `CALENDAR_PROVIDER_MICROSOFT_ENABLED`, `IS_MICROSOFT_SYNC_ENABLED`
### v0.40.0 la v0.41.0
Actualizați instanța Twenty pentru a utiliza imaginea v0.41.0
```
yarn database:migrate:prod
yarn command:prod upgrade-0.41
```
**Variabile de mediu**
* Eliminat: `AUTH_MICROSOFT_TENANT_ID`
### v0.35.0 la v0.40.0
Actualizați instanța Twenty pentru a utiliza imaginea v0.40.0
```
yarn database:migrate:prod
yarn command:prod upgrade-0.40
```
**Variabile de mediu**
* Adăugat: `IS_EMAIL_VERIFICATION_REQUIRED`, `EMAIL_VERIFICATION_TOKEN_EXPIRES_IN`, `WORKFLOW_EXEC_THROTTLE_LIMIT`, `WORKFLOW_EXEC_THROTTLE_TTL`
### v0.34.0 la v0.35.0
Actualizați instanța Twenty pentru a utiliza imaginea v0.35.0
```
yarn database:migrate:prod
yarn command:prod upgrade-0.35
```
Comanda `yarn database:migrate:prod` va aplica migrațiile pe structura bazei de date (schemele de core și metadata)
Comanda `yarn command:prod upgrade-0.35` se ocupă de migrația datelor pentru toate spațiile de lucru.
**Variabile de mediu**
* Am înlocuit `ENABLE_DB_MIGRATIONS` cu `DISABLE_DB_MIGRATIONS` (valoarea standard este acum `false`, probabil nu va trebui să setați nimic)
### v0.33.0 la v0.34.0
Actualizați instanța Twenty pentru a utiliza imaginea v0.34.0
```
yarn database:migrate:prod
yarn command:prod upgrade-0.34
```
Comanda `yarn database:migrate:prod` va aplica migrațiile pe structura bazei de date (schemele core și metadata)
Comanda `yarn command:prod upgrade-0.34` se ocupă de migrația datelor pentru toate spațiile de lucru.
**Variabile de mediu**
* Eliminat: `FRONT_BASE_URL`
* Adăugat: `FRONT_DOMAIN`, `FRONT_PROTOCOL`, `FRONT_PORT`
Am actualizat modul în care gestionăm URL-ul frontend.
Acum puteți seta URL-ul frontend utilizând variabilele `FRONT_DOMAIN`, `FRONT_PROTOCOL` și `FRONT_PORT`.
Dacă FRONT_DOMAIN nu este setat, URL-ul frontend va reveni la `SERVER_URL`.
### v0.32.0 la v0.33.0
Actualizați instanța Twenty pentru a utiliza imaginea v0.33.0
```
yarn command:prod cache:flush
yarn database:migrate:prod
yarn command:prod upgrade-0.33
```
Comanda `yarn command:prod cache:flush` va goli cache-ul Redis.
Comanda `yarn database:migrate:prod` va aplica migrațiile pe structura bazei de date (schemele core și metadata)
Comanda `yarn command:prod upgrade-0.33` se ocupă de migrația datelor pentru toate spațiile de lucru.
Începând cu această versiune, imaginea twenty-postgres pentru DB a devenit depășită și se utilizează twenty-postgres-spilo.
Dacă doriți să păstrați utilizarea imaginii twenty-postgres, înlocuiți pur și simplu `twentycrm/twenty-postgres:${TAG}` cu `twentycrm/twenty-postgres` în docker-compose.yml.
### v0.31.0 la v0.32.0
Actualizați instanța Twenty pentru a utiliza imaginea v0.32.0
**Migrarea schemelor și datelor**
```
yarn database:migrate:prod
yarn command:prod upgrade-0.32
```
Comanda `yarn database:migrate:prod` va aplica migrațiile pe structura bazei de date (schemele core și metadata)
Comanda `yarn command:prod upgrade-0.32` se ocupă de migrația datelor pentru toate spațiile de lucru.
**Variabile de mediu**
Am actualizat modul în care gestionăm conexiunea Redis.
* Eliminat: `REDIS_HOST`, `REDIS_PORT`, `REDIS_USERNAME`, `REDIS_PASSWORD`
* Adăugat: `REDIS_URL`
Actualizați fișierul `.env` pentru a utiliza noua variabilă `REDIS_URL` în locul parametrilor individuali de conexiune Redis.
Am simplificat, de asemenea, modul în care gestionăm jetoanele JWT.
* Eliminat: `ACCESS_TOKEN_SECRET`, `LOGIN_TOKEN_SECRET`, `REFRESH_TOKEN_SECRET`, `FILE_TOKEN_SECRET`
* Adăugat: `APP_SECRET`
Actualizați fișierul `.env` pentru a utiliza noua variabilă `APP_SECRET` în locul secretelor individuale ale tokenurilor (puteți folosi același secret ca și înainte sau generați unul nou aleator).
**Cont conectat**
Dacă utilizați un cont conectat pentru a sincroniza emailurile și calendarele Google, va trebui să activați [People API](https://developers.google.com/people) în consola dvs. Google Admin.
### v0.30.0 la v0.31.0
Actualizați instanța Twenty pentru a utiliza imaginea v0.31.0
**Migrarea schemelor și datelor**:
```
yarn database:migrate:prod
yarn command:prod upgrade-0.31
```
Comanda `yarn database:migrate:prod` va aplica migrațiile pe structura bazei de date (schemele core și metadata)
Comanda `yarn command:prod upgrade-0.31` se ocupă de migrația datelor pentru toate spațiile de lucru.
### v0.24.0 la v0.30.0
Actualizați instanța Twenty pentru a utiliza imaginea v0.30.0
**Schimbare importantă**:
Pentru a îmbunătăți performanțele, Twenty necesită acum cache de tip Redis configurat. Am actualizat [docker-compose.yml](https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/docker-compose.yml) pentru a reflecta acest lucru.
Asigurați-vă că vă actualizați configurația și actualizați variabilele de mediu corespunzător:
```
REDIS_HOST={your-redis-host}
REDIS_PORT={your-redis-port}
CACHE_STORAGE_TYPE=redis
```
**Migrarea schemelor și datelor**:
```
yarn database:migrate:prod
yarn command:prod upgrade-0.30
```
Comanda `yarn database:migrate:prod` va aplica migrațiile pe structura bazei de date (schemele core și metadata)
Comanda `yarn command:prod upgrade-0.30` se ocupă de migrația datelor pentru toate spațiile de lucru.
### v0.23.0 la v0.24.0
Actualizați instanța Twenty pentru a utiliza imaginea v0.24.0
Rulați următoarele comenzi:
```
yarn database:migrate:prod
yarn command:prod upgrade-0.24
```
Comanda `yarn database:migrate:prod` va aplica migrațiile pe structura bazei de date (schemele core și metadata)
Comanda `yarn command:prod upgrade-0.24` se ocupă de migrația datelor pentru toate spațiile de lucru.
### v0.22.0 la v0.23.0
Actualizați instanța Twenty pentru a utiliza imaginea v0.23.0
Rulați următoarele comenzi:
```
yarn database:migrate:prod
yarn command:prod upgrade-0.23
```
Comanda `yarn database:migrate:prod` va aplica migrațiile pe baza de date.
Comanda `yarn command:prod upgrade-0.23` se ocupă de migrația datelor, inclusiv transferul activităților către sarcini/note.
### v0.21.0 la v0.22.0
Actualizați instanța Twenty pentru a utiliza imaginea v0.22.0
Rulați următoarele comenzi:
```
yarn database:migrate:prod
yarn command:prod workspace:sync-metadata -f
yarn command:prod upgrade-0.22
```
Comanda `yarn database:migrate:prod` va aplica migrațiile pe baza de date.
Comanda `yarn command:prod workspace:sync-metadata -f` va sincroniza definiția obiectelor standard cu tabelele de metadate și va aplica migrațiile necesare în spațiile de lucru existente.
Comanda `yarn command:prod upgrade-0.22` va aplica transformări specifice de date pentru a se adapta la noile opțiuni defaultRequestInstrumentationOptions ale obiectului.
+82 -51
View File
@@ -1,27 +1,24 @@
{
"tabs": {
"gettingStarted": {
"label": "Începeți",
"groups": {
"welcome": {
"label": "Bun venit"
},
"coreConcepts": {
"label": "Concepte cheie"
}
}
},
"userGuide": {
"label": "User Guide",
"groups": {
"userGuideOverview": {
"label": "Prezentare generală"
"discoverTwenty": {
"label": "Discover Twenty",
"groups": {
"gettingStartedCapabilities": {
"label": "Capabilities"
},
"gettingStartedHowTos": {
"label": "How-Tos"
}
}
},
"dataModel": {
"label": "Model de date",
"groups": {
"dataModelReference": {
"label": "Referință"
"dataModelCapabilities": {
"label": "Capabilities"
},
"dataModelHowTos": {
"label": "How-Tos"
@@ -31,8 +28,8 @@
"dataMigration": {
"label": "Data Migration",
"groups": {
"dataMigrationReference": {
"label": "Referință"
"dataMigrationCapabilities": {
"label": "Capabilities"
},
"dataMigrationHowTos": {
"label": "How-Tos"
@@ -42,8 +39,8 @@
"calendarEmails": {
"label": "Calendar & Emails",
"groups": {
"calendarEmailsReference": {
"label": "Referință"
"calendarEmailsCapabilities": {
"label": "Capabilities"
},
"calendarEmailsHowTos": {
"label": "How-Tos"
@@ -53,8 +50,8 @@
"workflows": {
"label": "Fluxuri de lucru",
"groups": {
"workflowsReference": {
"label": "Referință"
"workflowsCapabilities": {
"label": "Capabilities"
},
"workflowsHowTos": {
"label": "How-Tos",
@@ -78,35 +75,30 @@
"ai": {
"label": "AI",
"groups": {
"aiReference": {
"label": "Referință"
"aiCapabilities": {
"label": "Capabilities"
},
"aiHowTos": {
"label": "How-Tos"
}
}
},
"layout": {
"label": "Aspect",
"viewsPipelines": {
"label": "Vizualizări și fluxuri",
"groups": {
"layoutReference": {
"label": "Referință",
"groups": {
"layoutViews": {
"label": "Vizualizări"
}
}
"viewsPipelinesCapabilities": {
"label": "Capabilities"
},
"layoutHowTos": {
"label": "Ghiduri practice"
"viewsPipelinesHowTos": {
"label": "How-Tos"
}
}
},
"dashboards": {
"label": "Tablouri de Bord",
"groups": {
"dashboardsReference": {
"label": "Referință"
"dashboardsCapabilities": {
"label": "Capabilities"
},
"dashboardsHowTos": {
"label": "How-Tos"
@@ -116,8 +108,8 @@
"permissionsAccess": {
"label": "Permissions & Access",
"groups": {
"permissionsAccessReference": {
"label": "Referință"
"permissionsAccessCapabilities": {
"label": "Capabilities"
},
"permissionsAccessHowTos": {
"label": "How-Tos"
@@ -127,8 +119,8 @@
"billing": {
"label": "Facturare",
"groups": {
"billingReference": {
"label": "Referință"
"billingCapabilities": {
"label": "Capabilities"
},
"billingHowTos": {
"label": "How-Tos"
@@ -138,8 +130,8 @@
"settings": {
"label": "Setări",
"groups": {
"settingsReference": {
"label": "Referință"
"settingsCapabilities": {
"label": "Capabilities"
},
"settingsHowTos": {
"label": "How-Tos"
@@ -151,20 +143,59 @@
"developers": {
"label": "Dezvoltatori",
"groups": {
"developersOverview": {
"label": "Prezentare generală"
"developersGroup": {
"label": "Dezvoltatori"
},
"apps": {
"label": "Aplicații"
},
"api": {
"label": "API"
"extend": {
"label": "Extend",
"groups": {
"apps": {
"label": "Aplicații"
}
}
},
"selfHost": {
"label": "Self-Host"
"label": "Self-Host",
"groups": {
"selfHostCapabilities": {
"label": "Capabilities"
}
}
},
"contribute": {
"label": "Contribute"
"label": "Contribute",
"groups": {
"contributeCapabilities": {
"label": "Capabilities",
"groups": {
"frontendDevelopment": {
"label": "Frontend Development",
"groups": {
"twentyUi": {
"label": "Twenty UI",
"groups": {
"display": {
"label": "Afișare"
},
"feedback": {
"label": "Feedback"
},
"input": {
"label": "Intrare"
},
"navigation": {
"label": "Navigare"
}
}
}
}
},
"backendDevelopment": {
"label": "Dezvoltare Backend"
}
}
}
}
}
}
}
@@ -1,6 +1,5 @@
---
title: Sfat aplicație
icon: mesaj
---
<Frame>
@@ -1,6 +1,5 @@
---
title: Bifă
icon: circle-check
---
<Frame>
@@ -1,6 +1,5 @@
---
title: Pictograme
icon: pictograme
---
<Frame>
@@ -2,6 +2,7 @@
title: Pastilă „În curând”
---
O insignă mică sau "pastilă" pentru a indica faptul că ceva este pe cale să vină în curând.
```jsx
@@ -1,8 +1,8 @@
---
title: Etichetă
icon: etichetă
---
Componentă pentru a categoriza vizual sau a eticheta conținutul.
<Tabs>
@@ -1,6 +1,5 @@
---
title: Butoane
icon: cursor mână
---
<Frame>
@@ -1,6 +1,5 @@
---
title: Bifă
icon: square-check
---
<Frame>
@@ -1,6 +1,5 @@
---
title: Schema culorilor
icon: paletă
---
<Frame>
@@ -1,6 +1,5 @@
---
title: Radio
icon: circle-dot
---
<Frame>
@@ -1,8 +1,8 @@
---
title: Comutare
icon: toggle-on
---
<Tabs>
<Tab title="Utilizare">
@@ -1,6 +1,5 @@
---
title: Prezentare generală
icon: paletă
description: Bibliotecă de componente pentru Twenty CRM
---
@@ -1,6 +1,5 @@
---
title: Navigare
icon: busolă
---
<Frame>
@@ -1,6 +1,5 @@
---
title: Linkuri
icon: link
---
<Frame>
@@ -1,8 +1,8 @@
---
title: Element de meniu
icon: bars
---
Un element de meniu versatil, conceput pentru a fi utilizat într-un meniu sau listă de navigare.
<Tabs>
@@ -1,8 +1,8 @@
---
title: Bară de navigare
icon: bars
---
Redă o bară de navigare care conține mai multe componente `NavigationBarItem`.
<Tabs>
@@ -2,6 +2,7 @@
title: Feedback
---
Indică progresul sau numărătoarea inversă și se mișcă de la dreapta la stânga.
<Tabs>
@@ -1,32 +1,25 @@
---
title: Credite
description: Înțelegerea modului în care creditele alimentează fluxurile de lucru, agenții AI și chatbotul AI — și cum să vă gestionați soldul.
title: Credite pentru fluxuri de lucru
description: Înțelegerea creditelor pentru fluxurile de lucru, a consumului și a modului de a cumpăra mai multe.
---
## Prezentare generală
Creditele alimentează automatizările fluxurilor de lucru și capabilitățile AI. Acestea sunt consumate atunci când acțiunile din fluxurile de lucru se execută, când agenții AI procesează sarcini în cadrul fluxurilor de lucru și în chatul AI.
Creditele alimentează automatizările fluxurilor de lucru în Twenty. Fiecare acțiune de flux de lucru consumă credite în funcție de complexitatea sa.
## Alocarea creditelor
Creditele se bazează pe ciclul de facturare, nu pe planul dvs.:
| Ciclu de facturare | Credite |
| ------------------ | ------- |
| Lunar | 5/lună |
| Anual | 50/an |
| Ciclu de facturare | Credite |
| ------------------ | ----------------- |
| Lunar | 5 milioane/lună |
| Anual | 50 de milioane/an |
<Note>
Cele 5 credite lunare sunt concepute pentru a vă permite să rulați automatizări fără să vă faceți griji în privința costurilor. Pentru majoritatea fluxurilor de lucru care folosesc acțiuni standard, acest lucru este mai mult decât suficient. Veți avea nevoie de credite suplimentare doar atunci când rulați noduri de cod avansate sau funcționalități bazate pe AI.
Cele 5 milioane de credite lunare sunt concepute pentru a vă permite să rulați automatizări fără să vă faceți griji pentru costuri. Pentru majoritatea fluxurilor de lucru care folosesc acțiuni standard, acest lucru este mai mult decât suficient. Veți avea nevoie de credite suplimentare doar atunci când rulați noduri de cod avansate sau funcționalități bazate pe AI.
</Note>
## Reportare de credite
Creditele neutilizate la sfârșitul unei perioade de facturare sunt reportate automat în perioada următoare.
* **Limită**: Suma reportată este limitată la alocarea completă a unei perioade, astfel încât nu veți putea reporta mai multe credite decât oferă planul dvs. pe perioadă.
* **Vizibilitate**: Când există credite reportate, acestea apar ca o linie separată **Credite reportate** în **Setări → Facturare**, alături de un sold **Total disponibil**.
## Consumul de credite
Acțiunile diferite consumă cantități diferite de credite:
@@ -35,9 +28,9 @@ Acțiunile diferite consumă cantități diferite de credite:
| --------------------------------------------------------------------- | --------------------------------- |
| **Operațiuni de bază** (căutare, actualizare, creare de înregistrări) | Minim |
| **Operațiuni complexe** (noduri de cod, apeluri API externe) | Mai multe credite |
| **Prompturi AI și chaturi AI** | Variabil, în funcție de utilizare |
| **Indicațiile AI** (în curând) | Variabil, în funcție de utilizare |
Creditele sunt retrase în timp real când se execută fluxurile de lucru.
Creditele sunt retrase în timp real pe măsură ce fluxurile de lucru se execută.
## Monitorizarea utilizării
@@ -19,7 +19,7 @@ Pentru echipe pregătite să se extindă:
* Asistență standard
<Note>
Funcționalitățile Premium (SSO, permisiuni la nivel de rând și date privind utilizarea AI) nu sunt incluse în planul Pro.
Funcționalitățile premium (SSO și permisiuni la nivel de rând) nu sunt incluse în planul Pro.
</Note>
### Organization (Cloud)
@@ -27,7 +27,7 @@ Funcționalitățile Premium (SSO, permisiuni la nivel de rând și date privind
Pentru echipe mai mari cu nevoi avansate:
* Tot ce include Pro
* **Funcționalități Premium**: integrare SSO, permisiuni la nivel de rând și date privind utilizarea AI
* **Funcționalități premium**: integrare SSO și permisiuni la nivel de rând
* Suport prioritar
## Planuri autogăzduite
@@ -45,7 +45,7 @@ Găzduiți Twenty pe propria infrastructură, fără costuri:
Pentru echipe care au nevoie de funcționalități premium în timp ce se autogăzduiesc:
* Toate funcționalitățile Pro
* **Funcționalități Premium**: integrare SSO, permisiuni la nivel de rând și date privind utilizarea AI
* **Funcționalități premium**: integrare SSO și permisiuni la nivel de rând
* Asistență din partea echipei Twenty
* Fără obligația de a publica codul personalizat ca open-source înainte de distribuire
@@ -55,7 +55,6 @@ Funcționalitățile premium sunt disponibile doar în planurile Organization (C
* **Integrare SSO**: autentificare unică (Single Sign-On) cu furnizorul dumneavoastră de identitate
* **Permisiuni la nivel de rând**: control de acces granular la nivel de înregistrare
* **Date privind utilizarea AI**: Urmăriți consumul de AI în spațiul de lucru
## Schimbarea planurilor
@@ -78,15 +77,3 @@ Contactați asistența pentru a face downgrade al planului.
### Treceți la facturarea lunară
Contactați asistența pentru a reveni la facturarea lunară.
## Obțineți o cheie Enterprise pentru Organization (Self-Hosted)
Pentru a utiliza planul Organization (Self-Hosted), trebuie să obțineți o cheie Enterprise:
1. Mergeți la **Setări → Panou Admin → Enterprise**
<img src="/images/user-guide/billing/enterprise-key.png" alt="Cheie Enterprise" />
2. Faceți clic pe **Obțineți cheia Enterprise**
3. Când sunteți redirecționați către Stripe, introduceți detaliile de plată și confirmați
4. Când este afișată cheia Enterprise, lipiți-o în pagina de setări Enterprise și activați licența organizației
@@ -16,11 +16,6 @@ Dacă vrei să găzduiești pe cont propriu și ai nevoie de funcțiile Premium
Funcțiile Premium sunt disponibile doar în planurile Organization (Cloud sau Self-Hosted):
* **Integrare SSO**: Autentificare unică cu furnizorul tău de identitate
* **Permisiuni la nivel de rând**: Control de acces granular la nivel de înregistrare
* **Date privind utilizarea AI**: Urmăriți consumul de AI în spațiul de lucru
</Accordion>
<Accordion title="Planul Organization în cloud și planul Organization autogăzduit sunt același lucru?">
Oferă aceleași funcții Premium. Totuși, un abonament în cloud nu poate fi folosit pentru implementări autogăzduite. Varianta autogăzduită necesită o cheie Enterprise.
</Accordion>
<Accordion title="Oferiți locuri gratuite pentru utilizatorii care doar vizualizează?">
@@ -28,7 +23,7 @@ Nu oferim locuri gratuite. Prețul este pe utilizator și fiecare utilizator are
</Accordion>
<Accordion title="Unde pot schimba abonamentul meu la planul Organization?">
Poți face acest lucru în `Setări → Facturare`. Apoi, fă clic pe `Schimbă la Organization`.
Poți face acest lucru sub `Setări → Facturare`. Apoi, fă clic pe `Schimbă la Organization`.
</Accordion>
<Accordion title="Unde pot schimba abonamentul meu la planul Pro?">
@@ -49,8 +44,8 @@ Vei găsi acest lucru sub `Setări → Facturare`.
<Accordion title="Câte credite de flux de lucru avem?">
Numărul de credite depinde de ciclul tău de facturare, nu de planul tău:
* **Abonamente lunare**: 5 credite pe lună
* **Abonamente anuale**: 50 de credite pe an
* **Abonamente lunare**: 5 milioane de credite pe lună
* **Abonamente anuale**: 50 de milioane de credite pe an
</Accordion>
<Accordion title="Cum funcționează consumul de credite de flux de lucru?">
@@ -3,6 +3,7 @@ title: Facturare
description: Înțelegeți prețurile Twenty și gestionați-vă abonamentul.
---
Twenty oferă planuri de preț flexibile care se potrivesc nevoilor echipei dvs. Gestionați-vă abonamentul, urmăriți creditele pentru fluxuri de lucru și accesați facturile, toate din **Setări → Facturare**.
## Ce conține această secțiune
@@ -3,6 +3,7 @@ title: Calendar și e-mailuri
description: Conectați-vă conturile de e-mail și calendar la Twenty.
---
## Opțiuni de Conexiune
### Cont Google (Gmail & Calendar Google)
@@ -3,6 +3,7 @@ title: Tablouri de Bord
description: Învățați noțiunile de bază despre raportare și tablouri de bord în Twenty.
---
<Note>
Tablourile de bord sunt în prezent în versiune beta. Activați-le din **Setări → Actualizări → Acces timpuriu**.
</Note>
@@ -139,13 +139,13 @@ The data doesn't match the expected format for that field type.
#### Dată
**Problemă:** Format de dată nerecunoscut
**Soluție:** Folosește formatul `YYYY-MM-DD` în mod consecvent în întregul fișier
**Problem:** Unrecognized date format
**Solution:** Use consistent format throughout file
```
✓ 2024-03-15 (YYYY-MM-DD)
03/15/2024 (MM/DD/YYYY)
15/03/2024 (DD/MM/YYYY)
✓ 2024-03-15 (YYYY-MM-DD - recommended)
03/15/2024 (MM/DD/YYYY)
15/03/2024 (DD/MM/YYYY)
```
#### Telefon
@@ -103,6 +103,8 @@ Adresa este un **câmp îmbricat** cu mai multe coloane (unele pot fi lăsate ne
Folosiți un format consecvent în tot fișierul:
* `YYYY-MM-DD` (recomandat): `2024-03-15`
* `MM/DD/YYYY`: `03/15/2024`
* `DD/MM/YYYY`: `15/03/2024`
* ISO 8601: `2024-03-15T10:30:00Z`
### Câmpuri numerice
@@ -5,6 +5,7 @@ description: Importați și exportați datele CRM prin fișiere CSV sau API.
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
## Metode de import
Twenty acceptă două metode principale de importare a datelor:
@@ -53,45 +53,38 @@ Mai întâi, creați obiectul intermediar care va păstra conexiunile.
1. Accesați **Setări → Model de date**
2. Faceți clic pe **+ Obiect nou**
3. Dați-i un nume descriptiv (de ex., "Atribuire proiect", "Membru al echipei", "Comandă de produs")
4. Activați "Omite crearea unui câmp Nume"
<img src="/images/user-guide/fields/new-pivot-object.png" alt="Obiect pivot nou" />
5. Faceți clic pe **Salvare**
4. Faceți clic pe **Salvare**
<Tip>
**Convenție de denumire**: Folosiți un nume care descrie relația, cum ar fi "Atribuire proiect" sau "Apartenență la echipă". Acest lucru face modelul de date mai ușor de înțeles.
</Tip>
## Pasul 2: Creați relații între obiecte și obiectul de legătură
## Pasul 2: Creați relații din obiectul de legătură
Adăugați câmpuri de relație din fiecare dintre cele două obiecte la obiectul de legătură.
Adăugați câmpuri de relație din obiectul de legătură către ambele obiecte pe care doriți să le conectați.
### Prima relație (Obiectul A → Obiect de legătură)
1. Selectați primul obiect în **Setări → Model de date**
2. Faceți clic pe **+ Adaugă relație**
3. Selectați obiectul de legătură (de ex., "Atribuiri proiect")
4. Setați tipul relației la **Unu-la-mulți** (o persoană poate fi asociată cu multe atribuiri)
5. Denumiți câmpurile:
* Câmp pe Persoane: de ex., "Atribuiri proiect"
* Câmp pe obiectul de legătură: de ex., "Persoană"
6. Faceți clic pe **Salvare**
### A doua relație (Obiectul B → Obiect de legătură)
1. Selectați al doilea obiect în **Setări → Model de date**
2. Faceți clic pe **+ Adaugă relație**
3. Selectați obiectul de legătură (de ex., "Atribuiri proiect")
4. Setați tipul relației la **Unu-la-mulți** (un proiect poate fi asociat cu multe atribuiri)
5. Activați **"Aceasta este o relație către un obiect de legătură"**
<img src="/images/user-guide/fields/junction-relation-toggle.png" style={{width:'100%'}} />
### Prima relație (Obiect de legătură → Obiectul A)
1. Selectați obiectul de legătură în **Setări → Model de date**
2. Faceți clic pe **+ Adaugă câmp**
3. Alegeți **Relație** ca tip de câmp
4. Selectați primul obiect (de ex., "Persoane")
5. Setați tipul relației la **Mulți-la-unu** (multe atribuiri pot fi legate de o singură persoană)
6. Denumiți câmpurile:
* Câmp pe obiectul de legătură: de ex., "Persoană"
* Câmp pe Persoane: de ex., "Atribuiri proiect"
7. Faceți clic pe **Salvare**
### A doua relație (Obiect de legătură → Obiectul B)
1. Rămânând pe obiectul de legătură, faceți clic pe **+ Adăugați câmp**
2. Alegeți **Relație** ca tip de câmp
3. Selectați al doilea obiect (de ex., "Proiecte")
4. Setați tipul relației la **Mulți-la-unu**
5. Denumiți câmpurile:
* Câmp pe obiectul de legătură: de ex., "Proiect"
* Câmp pe Proiecte: de ex., "Membrii echipei"
7. Faceți clic pe **Salvare**
6. Faceți clic pe **Salvare**
## Pasul 3: Configurați afișarea relației de joncțiune
@@ -101,10 +94,22 @@ Acum configurați obiectele sursă pentru a afișa direct înregistrările legat
2. Selectați primul obiect (de ex., "Persoane")
3. Găsiți câmpul de relație care indică spre obiectul de legătură (de ex., "Atribuiri proiect")
4. Faceți clic pentru a edita câmpul
5. Activați **"Aceasta este o relație cu un obiect de legătură"**
5. Activați **"Aceasta este o relație către un obiect de legătură"**
6. Selectați **Relația țintă** (de ex., "Proiect" — câmpul de pe obiectul de legătură care indică spre cealaltă parte)
7. Faceți clic pe **Salvare**
{/* TODO: Add image
<img src="/images/user-guide/fields/junction-relation-toggle.png" style={{width:'100%'}}/>
*/}
Repetați pentru celălalt obiect:
1. Selectați "Proiecte" în Model de date
2. Editați câmpul de relație "Membrii echipei"
3. Activați comutatorul de joncțiune
4. Selectați "Persoană" ca relație țintă
5. Salvează
## Rezultat
După configurare:
@@ -125,15 +130,15 @@ Iată un ghid complet:
### Adăugați relații
1. **Persoane → Atribuire proiect**
* Tip: Unu-la-mulți
* Câmp pe Persoane: "Atribuiri proiect"
1. **Atribuire proiect → Persoane**
* Tip: Mulți-la-unu
* Câmp pe Atribuire proiect: "Persoană"
* Câmp pe Persoane: "Atribuiri proiect"
2. **Proiecte → Atribuire proiect**
* Tip: Unu-la-mulți
* Câmp pe Proiecte: "Membrii echipei"
2. **Atribuire proiect → Proiecte**
* Tip: Mulți-la-unu
* Câmp pe Atribuire proiect: "Proiect"
* Câmp pe Proiecte: "Membrii echipei"
### Configurați afișarea relației de joncțiune
@@ -3,6 +3,7 @@ title: Model de date
description: Află ce este un model de date și cum să proiectezi unul care se potrivește afacerii tale.
---
## Ce este un model de date?
Un model de date este structura care definește cum sunt organizate informațiile în CRM-ul tău. Gândește-te la el ca la **planul** datelor tale despre clienți — îl proiectezi o singură dată, apoi îl umpli cu datele tale reale.
@@ -1,11 +1,16 @@
---
title: Ghid Utilizatori
title: Descoperiți Twenty
description: Bine ați venit în Ghidul utilizatorului Twenty, resursa dvs. pentru configurări avansate și bune practici.
---
import { CardTitle } from "/snippets/card-title.mdx"
<CardGroup cols={3}>
<Card href="/l/ro/user-guide/getting-started/capabilities/what-is-twenty" img="/images/user-guide/api/api.png">
<CardTitle>Descoperiți Twenty</CardTitle>
Aflați ce este Twenty și cum vă poate ajuta afacerea.
</Card>
<Card href="/l/ro/user-guide/data-model/overview" img="/images/user-guide/halftone/data-model.png">
<CardTitle>Model de date</CardTitle>
Personalizați modelul de date pentru a se potrivi proceselor dvs. de afaceri.
@@ -31,9 +36,9 @@ import { CardTitle } from "/snippets/card-title.mdx"
Îmbunătățiți-vă echipa cu agenți IA.
</Card>
<Card href="/l/ro/user-guide/layout/overview" img="/images/user-guide/halftone/layout.png">
<CardTitle>Layout</CardTitle>
Navigare, vizualizări și personalizarea paginii înregistrării.
<Card href="/l/ro/user-guide/views-pipelines/overview" img="/images/user-guide/halftone/layout.png">
<CardTitle>Vizualizări și pipeline-uri</CardTitle>
Organizați-vă datele cu vizualizări acționabile și pipeline-uri.
</Card>
<Card href="/l/ro/user-guide/dashboards/overview" img="/images/user-guide/halftone/dashboards.png">
@@ -1,38 +0,0 @@
---
title: Navigare
description: Personalizează bara laterală din stânga pentru a se potrivi modului în care lucrează echipa ta.
---
Bara laterală din stânga este modalitatea principală de a naviga în Twenty. Este complet personalizabilă — o poți reorganiza astfel încât să se potrivească fluxului tău de lucru, fără să atingi vreo pagină de setări.
## Reordonarea elementelor
Trage și plasează orice element din bara laterală pentru a-i schimba poziția. Ordinea este salvată per utilizator, astfel încât fiecare membru al echipei își poate aranja propria bară laterală.
## Foldere
Grupează elemente înrudite în foldere. De exemplu, poți crea un folder "Sales" care să conțină vizualizările de pipeline, un folder "Support" pentru tichete sau un folder "Operations" pentru obiecte interne.
Pentru a crea un folder, fă clic dreapta în bara laterală sau folosește butonul `+`.
## Ascunderea obiectelor
Obiectele pe care nu le folosești pot fi ascunse din bara laterală. Nu sunt șterse — doar sunt scoase din cale. Le poți afișa din nou oricând din Setări > Model de Date.
## Favorite
Fixează vizualizări, înregistrări sau căutări în secțiunea Favorite din partea de sus a barei laterale pentru acces cu un singur clic. Favoritele sunt personale — fiecare utilizator le gestionează pe ale sale.
## Linkuri personalizate
Adaugă linkuri către instrumente externe direct în bara laterală. Util pentru a face legătura către wiki-ul tău, dashboarduri din alte instrumente sau orice URL pe care echipa ta îl folosește în mod regulat.
## Meniu de comenzi
Apasă `Cmd+K` (sau `Ctrl+K`) pentru a deschide meniul de comenzi — o bară de căutare cu acces rapid pentru a sări la orice înregistrare, vizualizare sau acțiune fără a naviga prin bara laterală.
## Personalizarea barei laterale
Pentru a personaliza bara laterală, treceți cursorul peste secțiunea "Spațiu de lucru" din bara laterală și faceți clic pe pictograma cu cheia.
<img src="/images/user-guide/layout/navigation-edit-icon.png" alt="Pictogramă de editare a navigării" />
@@ -1,61 +0,0 @@
---
title: Pagini de înregistrare
description: Personalizați aspectul paginilor de detalii ale înregistrărilor individuale cu file și widgeturi.
---
## Prezentare generală
Când deschideți o înregistrare în Twenty, pagina de detalii este alcătuită din **file** și **widgeturi**. Ambele sunt complet personalizabile pentru fiecare tip de obiect.
## Taburi
Fiecare pagină de înregistrare poate avea mai multe file — similare cu filele dintr-un browser. Folosiți-le pentru a organiza diferite aspecte ale unei înregistrări. De exemplu, o înregistrare Companie poate avea file pentru Prezentare generală, Comunicare, Sarcini și Fișiere.
Puteți:
* Adăugați și eliminați file
* Redenumiți filele
* Reordonați filele prin glisare
* Setați care filă este afișată în mod implicit
## Widgeturi
Widgeturile sunt elementele de bază din fiecare filă. Tipuri de widgeturi disponibile:
| Widget | Ce afișează |
| ------------------------- | -------------------------------------------------- |
| **Câmpuri** | Câmpuri ale înregistrării, grupate sau individuale |
| **Înregistrări asociate** | Tabel cu înregistrări legate printr-o relație |
| **Email-uri** | Istoricul emailurilor din conturile conectate |
| **Calendar** | Evenimente de calendar asociate înregistrării |
| **Cronologie** | Istoricul activităților și evenimentelor |
| **Sarcini** | Sarcini asociate |
| **Notițe** | Notițe cu text îmbogățit |
| **Fișiere** | Atașamente de fișiere |
| **Grafice** | Date vizuale din înregistrări asociate |
| **iFrame** | Conținut extern încorporat |
| **Text îmbogățit** | Conținut static sau descrieri |
## Personalizarea unei pagini de înregistrare
1. Deschideți orice înregistrare
2. Apăsați `Cmd+K` și căutați "Edit record page layout"
sau
1. Accesați Setări > Model de date > obiectul dorit > Aspect
2. Faceți clic pe butonul "Personalizați pagina înregistrării" pentru acel obiect
3. Acum sunteți în modul de personalizare:
* **Adăugați widgeturi** din selectorul de widgeturi
* **Glisați widgeturile** pentru a le repoziționa pe grilă
* **Redimensionați widgeturile** trăgându-le marginile
* **Configurați câmpurile** afișate în fiecare widget
* **Gestionați filele** — adăugați, eliminați, redenumiți, reordonați
4. Salvați modificările — se aplică tuturor înregistrărilor din tipul respectiv de obiect
## Vizibilitatea câmpurilor
Într-un widget Câmpuri, puteți controla care câmpuri sunt vizibile și în ce ordine. Acest lucru vă permite să creați dispuneri concentrate — de exemplu, să afișați doar cele mai importante câmpuri în fila Prezentare generală și să plasați câmpurile detaliate într-o filă separată.
@@ -1,45 +0,0 @@
---
title: Aspect
description: Personalizați modul în care navigați, răsfoiți și vizualizați înregistrările în Twenty.
---
Aspectul Twenty este personalizabil la trei niveluri: modul în care navigați în aplicație, modul în care răsfoiți listele de înregistrări și ceea ce vedeți când deschideți o înregistrare individuală.
## Navigare
Bara laterală stângă este complet personalizabilă. Puteți:
* **Reordonați elementele** prin glisare și plasare
* **Creați dosare** pentru a grupa obiecte și vizualizări înrudite
* **Ascundeți obiectele** pe care nu le folosiți
* **Adăugați linkuri personalizate** către instrumente externe
* **Fixați elemente favorite** pentru acces rapid la vizualizări, înregistrări sau căutări
[Referință de navigare →](/l/ro/user-guide/layout/capabilities/navigation)
## Vizualizări
Vizualizările controlează modul în care sunt afișate listele de înregistrări. Twenty acceptă trei tipuri de vizualizări:
| Vizualizare | Cel mai potrivit pentru |
| ------------ | ----------------------------------------------------------------------------------- |
| **Tabel** | Lucrul cu multe înregistrări deodată — rânduri și coloane în stil foaie de calcul |
| **Kanban** | Urmărirea pipeline-ului — carduri organizate pe etape, cu glisare și plasare |
| **Calendar** | Planificare în funcție de timp — înregistrări afișate în funcție de un câmp de dată |
Fiecare vizualizare își salvează propriile filtre, ordonarea, vizibilitatea câmpurilor și configurația de grupare. Vizualizările pot fi partajate cu spațiul de lucru sau păstrate private.
[Vizualizări de tip tabel →](/l/ro/user-guide/views-pipelines/capabilities/table-views) · [Vizualizări Kanban →](/l/ro/user-guide/views-pipelines/capabilities/kanban-views) · [Vizualizare calendar →](/l/ro/user-guide/views-pipelines/capabilities/calendar-view)
## Pagini de înregistrare
Când deschideți o înregistrare, pagina de detalii este construită din file și widgeturi configurabile. Puteți:
* **Adăugați, eliminați și reordonați filele** pentru orice tip de înregistrare
* **Configurați widgeturi** — câmpuri, înregistrări asociate, e-mailuri, cronologie, calendar, sarcini, note, fișiere, diagrame, iframe-uri și altele
* **Glisați și redimensionați widgeturile** pe o dispunere în grilă
* **Controlați vizibilitatea câmpurilor** pentru fiecare widget
Intrați în modul de personalizare a aspectului din meniul de comenzi (`Cmd+K` → "Editare aspect pagină de înregistrare").
[Referință pentru paginile de înregistrare →](/l/ro/user-guide/layout/capabilities/record-pages)
@@ -3,6 +3,7 @@ title: Permisiuni și acces
description: Gestionați rolurile, permisiunile și controlul accesului în spațiul dvs. de lucru.
---
Sistemul de permisiuni Twenty vă permite să controlați cine poate accesa și modifica datele din spațiul dvs. de lucru. Creați roluri, atribuiți permisiuni și configurați SSO pentru acces securizat.
## Ce conține această secțiune
@@ -3,6 +3,7 @@ title: Setări
description: Configurați spațiul de lucru Twenty cu configurările esențiale.
---
## Configurare inițială
Când creați pentru prima dată spațiul de lucru, există câteva setări cheie de configurat.
@@ -5,6 +5,7 @@ description: Aflați cum să creați și să gestionați vizualizările în Twen
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
## Înțelegerea vizualizărilor
Vizualizările sunt configurații salvate care determină cum sunt afișate datele dvs. Fiecare vizualizare poate avea propriile setări:
@@ -9,13 +9,13 @@ Creditele fluxului de lucru stimulează automatizările dvs. în Twenty. Înțel
Creditele pentru fluxurile de lucru sunt alocate în funcție de ciclul de facturare, nu de nivelul planului:
| Ciclu de facturare | Credite |
| ------------------- | ------------------- |
| **Abonament lunar** | 5 credite pe lună |
| **Abonament anual** | 50 de credite pe an |
| Ciclu de facturare | Credite |
| ------------------- | ------------------------------- |
| **Abonament lunar** | 5 milioane de credite pe lună |
| **Abonament anual** | 50 de milioane de credite pe an |
<Note>
5 credite lunare sunt generoase pentru automatizările standard. Majoritatea echipelor nu vor depăși această limită cu utilizarea tipică a fluxurilor de lucru. Credite suplimentare sunt necesare în principal pentru acțiuni Code avansate și pentru fluxuri de lucru bazate pe AI.
5 milioane de credite lunare sunt generoase pentru automatizările standard. Majoritatea echipelor nu vor depăși această limită cu utilizarea tipică a fluxurilor de lucru. Credite suplimentare sunt necesare în principal pentru acțiuni Code avansate și pentru fluxuri de lucru bazate pe AI.
</Note>
## Cum funcționează consumul de credite

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