Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f316902ea |
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git stash:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
.git
|
||||
.env
|
||||
**/node_modules
|
||||
node_modules
|
||||
.nx/cache
|
||||
packages/twenty-server/.env
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
name: AI Catalog Sync
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 6 * * *' # Daily at 6 AM UTC
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
sync-catalog:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=4096'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
|
||||
- name: Build dependencies
|
||||
run: npx nx build twenty-shared
|
||||
|
||||
- name: Run catalog sync
|
||||
run: npx nx run twenty-server:ts-node-no-deps-transpile-only -- ./scripts/ai-sync-models-dev.ts
|
||||
|
||||
- name: Check for changes
|
||||
id: changes
|
||||
run: |
|
||||
if git diff --quiet packages/twenty-server/src/engine/metadata-modules/ai/ai-models/ai-providers.json; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Create pull request
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: 'chore: sync AI model catalog from models.dev'
|
||||
title: 'chore: sync AI model catalog from models.dev'
|
||||
body: |
|
||||
Automated daily sync of `ai-providers.json` from [models.dev](https://models.dev).
|
||||
|
||||
This PR updates pricing, context windows, and model availability based on the latest data.
|
||||
New models meeting inclusion criteria (tool calling, pricing data, context limits) are added automatically.
|
||||
Deprecated models are detected based on cost-efficiency within the same model family.
|
||||
|
||||
**Please review before merging** — verify no critical models were incorrectly deprecated.
|
||||
branch: chore/ai-catalog-sync
|
||||
base: main
|
||||
labels: ai, automated
|
||||
delete-branch: true
|
||||
|
||||
- name: Trigger automerge
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
|
||||
repository: twentyhq/twenty-infra
|
||||
event-type: automated-pr-ready
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
create-twenty-app --version
|
||||
mkdir -p /tmp/e2e-test-workspace
|
||||
cd /tmp/e2e-test-workspace
|
||||
create-twenty-app test-app --exhaustive --display-name "Test App" --description "E2E test app" --skip-local-instance
|
||||
create-twenty-app test-app --exhaustive --display-name "Test App" --description "E2E test app"
|
||||
|
||||
- name: Install scaffolded app dependencies
|
||||
run: |
|
||||
@@ -151,24 +151,22 @@ jobs:
|
||||
SEED_API_KEY: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik'
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
npx --no-install twenty remote add --token $SEED_API_KEY --url http://localhost:3000
|
||||
npx --no-install twenty auth:login --api-key $SEED_API_KEY --api-url http://localhost:3000
|
||||
|
||||
- name: Build scaffolded app
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
npx --no-install twenty build
|
||||
npx --no-install twenty app:build
|
||||
test -d .twenty/output
|
||||
|
||||
- name: Execute hello-world logic function
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
EXEC_OUTPUT=$(npx --no-install twenty exec --functionName hello-world-logic-function)
|
||||
EXEC_OUTPUT=$(npx --no-install twenty function:execute --functionName hello-world-logic-function)
|
||||
echo "$EXEC_OUTPUT"
|
||||
echo "$EXEC_OUTPUT" | grep -q "Hello, World!"
|
||||
|
||||
- name: Run scaffolded app integration test
|
||||
env:
|
||||
TWENTY_API_URL: http://localhost:3000
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
yarn test
|
||||
|
||||
@@ -151,40 +151,6 @@ jobs:
|
||||
# npx nyc merge coverage-artifacts ${{ env.PATH_TO_COVERAGE }}/coverage-storybook.json
|
||||
# - name: Checking coverage
|
||||
# run: npx nx storybook:coverage twenty-front --checkCoverage=true --configuration=${{ matrix.storybook_scope }}
|
||||
visual-regression-dispatch:
|
||||
needs: front-sb-build
|
||||
if: github.event_name == 'pull_request'
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download storybook build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storybook-static
|
||||
path: storybook-static
|
||||
- name: Package storybook
|
||||
run: tar -czf /tmp/storybook-twenty-front.tar.gz -C storybook-static .
|
||||
- name: Upload storybook tarball
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: storybook-twenty-front-tarball
|
||||
path: /tmp/storybook-twenty-front.tar.gz
|
||||
retention-days: 1
|
||||
- name: Dispatch to ci-privileged
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
|
||||
repository: twentyhq/ci-privileged
|
||||
event-type: visual-regression
|
||||
client-payload: >-
|
||||
{
|
||||
"pr_number": "${{ github.event.pull_request.number }}",
|
||||
"run_id": "${{ github.run_id }}",
|
||||
"repo": "${{ github.repository }}",
|
||||
"project": "twenty-front",
|
||||
"branch": "${{ github.head_ref }}",
|
||||
"commit": "${{ github.event.pull_request.head.sha }}"
|
||||
}
|
||||
front-task:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
|
||||
@@ -10,8 +10,8 @@ permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'merge_group' && github.event.merge_group.base_ref || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name != 'merge_group' }}
|
||||
|
||||
jobs:
|
||||
e2e-test:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: CI Docker
|
||||
name: CI Docker Compose
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
files: |
|
||||
packages/twenty-docker/**
|
||||
docker-compose.yml
|
||||
test-compose:
|
||||
test:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
@@ -30,10 +30,10 @@ jobs:
|
||||
- name: Run compose
|
||||
run: |
|
||||
echo "Patching docker-compose.yml..."
|
||||
# change image to localbuild using yq
|
||||
yq eval 'del(.services.server.image)' -i docker-compose.yml
|
||||
yq eval '.services.server.build.context = "../../"' -i docker-compose.yml
|
||||
yq eval '.services.server.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i docker-compose.yml
|
||||
yq eval '.services.server.build.target = "twenty"' -i docker-compose.yml
|
||||
yq eval '.services.server.restart = "no"' -i docker-compose.yml
|
||||
|
||||
echo "Setting up .env file..."
|
||||
@@ -89,64 +89,11 @@ jobs:
|
||||
echo "Still waiting for server... (${count}/300s)"
|
||||
done
|
||||
working-directory: ./packages/twenty-docker/
|
||||
test-app-dev:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Create frontend placeholder
|
||||
run: |
|
||||
mkdir -p packages/twenty-front/build
|
||||
echo '<html><body>CI placeholder</body></html>' > packages/twenty-front/build/index.html
|
||||
- name: Build app-dev image
|
||||
run: |
|
||||
docker build \
|
||||
--target twenty-app-dev \
|
||||
-f packages/twenty-docker/twenty/Dockerfile \
|
||||
-t twenty-app-dev-ci \
|
||||
.
|
||||
- name: Start container
|
||||
run: |
|
||||
docker run -d --name twenty-app-dev \
|
||||
-p 3000:3000 \
|
||||
twenty-app-dev-ci
|
||||
docker logs twenty-app-dev -f &
|
||||
- name: Wait for server health
|
||||
run: |
|
||||
echo "Waiting for twenty-app-dev to become healthy..."
|
||||
count=0
|
||||
while true; do
|
||||
status=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/healthz 2>/dev/null || echo "000")
|
||||
if [ "$status" = "200" ]; then
|
||||
echo "Server is healthy!"
|
||||
curl -s http://localhost:3000/healthz
|
||||
break
|
||||
fi
|
||||
|
||||
container_status=$(docker inspect --format='{{.State.Status}}' twenty-app-dev 2>/dev/null || echo "unknown")
|
||||
if [ "$container_status" = "exited" ]; then
|
||||
echo "Container exited unexpectedly"
|
||||
docker logs twenty-app-dev
|
||||
exit 1
|
||||
fi
|
||||
|
||||
count=$((count+1))
|
||||
if [ $count -gt 300 ]; then
|
||||
echo "Server did not become healthy within 5 minutes"
|
||||
docker logs twenty-app-dev
|
||||
exit 1
|
||||
fi
|
||||
echo "Still waiting... (${count}/300s) [HTTP ${status}]"
|
||||
sleep 1
|
||||
done
|
||||
ci-test-docker-status-check:
|
||||
ci-test-docker-compose-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, test-compose, test-app-dev]
|
||||
needs: [changed-files-check, test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
name: CI UI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
merge_group:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
if: github.event_name != 'merge_group'
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
package.json
|
||||
yarn.lock
|
||||
packages/twenty-ui/**
|
||||
packages/twenty-shared/**
|
||||
ui-task:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
task: [lint, typecheck, test]
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/[email protected]
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Run ${{ matrix.task }}
|
||||
run: npx nx ${{ matrix.task }} twenty-ui
|
||||
ui-sb-build:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/[email protected]
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Build storybook
|
||||
run: npx nx storybook:build twenty-ui
|
||||
- name: Upload storybook build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: storybook-twenty-ui
|
||||
path: packages/twenty-ui/storybook-static
|
||||
retention-days: 1
|
||||
ui-sb-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
needs: ui-sb-build
|
||||
env:
|
||||
STORYBOOK_URL: http://localhost:6007
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Build dependencies
|
||||
run: npx nx build twenty-shared
|
||||
- name: Download storybook build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storybook-twenty-ui
|
||||
path: packages/twenty-ui/storybook-static
|
||||
- name: Install Playwright
|
||||
run: |
|
||||
cd packages/twenty-ui
|
||||
npx playwright install
|
||||
- name: Serve storybook & run tests
|
||||
run: |
|
||||
npx http-server packages/twenty-ui/storybook-static --port 6007 --silent &
|
||||
timeout 30 bash -c 'until curl -sf http://localhost:6007 > /dev/null 2>&1; do sleep 1; done'
|
||||
npx nx storybook:test twenty-ui
|
||||
visual-regression-dispatch:
|
||||
needs: ui-sb-build
|
||||
if: github.event_name == 'pull_request'
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download storybook build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storybook-twenty-ui
|
||||
path: storybook-static
|
||||
- name: Package storybook
|
||||
run: tar -czf /tmp/storybook-twenty-ui.tar.gz -C storybook-static .
|
||||
- name: Upload storybook tarball
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: storybook-twenty-ui-tarball
|
||||
path: /tmp/storybook-twenty-ui.tar.gz
|
||||
retention-days: 1
|
||||
- name: Dispatch to ci-privileged
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
|
||||
repository: twentyhq/ci-privileged
|
||||
event-type: visual-regression
|
||||
client-payload: >-
|
||||
{
|
||||
"pr_number": "${{ github.event.pull_request.number }}",
|
||||
"run_id": "${{ github.run_id }}",
|
||||
"repo": "${{ github.repository }}",
|
||||
"project": "twenty-ui",
|
||||
"branch": "${{ github.head_ref }}",
|
||||
"commit": "${{ github.event.pull_request.head.sha }}"
|
||||
}
|
||||
ci-ui-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
[
|
||||
changed-files-check,
|
||||
ui-task,
|
||||
ui-sb-build,
|
||||
ui-sb-test,
|
||||
]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
@@ -150,11 +150,3 @@ jobs:
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Trigger i18n automerge
|
||||
if: github.event_name != 'pull_request' && steps.check_changes.outputs.changes_detected == 'true'
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
|
||||
repository: twentyhq/twenty-infra
|
||||
event-type: i18n-pr-ready
|
||||
|
||||
@@ -138,11 +138,3 @@ jobs:
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Trigger i18n automerge
|
||||
if: steps.compile_translations.outputs.changes_detected == 'true'
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
|
||||
repository: twentyhq/twenty-infra
|
||||
event-type: i18n-pr-ready
|
||||
|
||||
@@ -102,11 +102,3 @@ jobs:
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Trigger i18n automerge
|
||||
if: steps.check_extract_changes.outputs.changes_detected == 'true' || steps.check_compile_changes.outputs.changes_detected == 'true'
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
|
||||
repository: twentyhq/twenty-infra
|
||||
event-type: i18n-pr-ready
|
||||
|
||||
@@ -24,12 +24,10 @@ jobs:
|
||||
yq eval 'del(.services.server.image)' -i packages/twenty-docker/docker-compose.yml
|
||||
yq eval '.services.server.build.context = "../../"' -i packages/twenty-docker/docker-compose.yml
|
||||
yq eval '.services.server.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i packages/twenty-docker/docker-compose.yml
|
||||
yq eval '.services.server.build.target = "twenty"' -i packages/twenty-docker/docker-compose.yml
|
||||
|
||||
yq eval 'del(.services.worker.image)' -i packages/twenty-docker/docker-compose.yml
|
||||
yq eval '.services.worker.build.context = "../../"' -i packages/twenty-docker/docker-compose.yml
|
||||
yq eval '.services.worker.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i packages/twenty-docker/docker-compose.yml
|
||||
yq eval '.services.worker.build.target = "twenty"' -i packages/twenty-docker/docker-compose.yml
|
||||
|
||||
echo "Adding SIGN_IN_PREFILLED environment variable to server service..."
|
||||
yq eval '.services.server.environment.SIGN_IN_PREFILLED = "${SIGN_IN_PREFILLED}"' -i packages/twenty-docker/docker-compose.yml
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"mcpServers": {
|
||||
"postgres": {
|
||||
"type": "stdio",
|
||||
"command": "bash",
|
||||
"args": ["-c", "source packages/twenty-server/.env && npx -y @modelcontextprotocol/server-postgres \"$PG_DATABASE_URL\""],
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-postgres", "${PG_DATABASE_URL}"],
|
||||
"env": {}
|
||||
},
|
||||
"playwright": {
|
||||
|
||||
@@ -80,17 +80,6 @@ npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/mi
|
||||
npx nx run twenty-server:command workspace:sync-metadata
|
||||
```
|
||||
|
||||
### Database Inspection (Postgres MCP)
|
||||
|
||||
A read-only Postgres MCP server is configured in `.mcp.json`. Use it to:
|
||||
- Inspect workspace data, metadata, and object definitions while developing
|
||||
- Verify migration results (columns, types, constraints) after running migrations
|
||||
- Explore the multi-tenant schema structure (core, metadata, workspace-specific schemas)
|
||||
- Debug issues by querying raw data to confirm whether a bug is frontend, backend, or data-level
|
||||
- Inspect metadata tables to debug GraphQL schema generation or `workspace:sync-metadata` issues
|
||||
|
||||
This server is read-only — for write operations (reset, migrations, sync), use the CLI commands above.
|
||||
|
||||
### GraphQL
|
||||
```bash
|
||||
# Generate GraphQL types (run after schema changes)
|
||||
|
||||
@@ -25,47 +25,47 @@ See Twenty application documentation https://docs.twenty.com/developers/extend/c
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 24+ (recommended) and Yarn 4
|
||||
- Docker (for the local Twenty dev server)
|
||||
- A Twenty workspace and an API key (create one at https://app.twenty.com/settings/api-webhooks)
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# Scaffold a new app — the CLI will offer to start a local Twenty server
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
# The scaffolder can automatically:
|
||||
# 1. Start a local Twenty server (Docker)
|
||||
# 2. Open the browser to log in ([email protected] / [email protected])
|
||||
# 3. Authenticate your app via OAuth
|
||||
# Get help and list all available commands
|
||||
yarn twenty help
|
||||
|
||||
# Or do it manually:
|
||||
yarn twenty server start # Start local Twenty server
|
||||
yarn twenty remote add --local # Authenticate via OAuth
|
||||
# Authenticate using your API key (you'll be prompted)
|
||||
yarn twenty auth:login
|
||||
|
||||
# Add a new entity to your application (guided)
|
||||
yarn twenty entity:add
|
||||
|
||||
# Start dev mode: watches, builds, and syncs local changes to your workspace
|
||||
yarn twenty dev
|
||||
# (also auto-generates typed CoreApiClient — MetadataApiClient ships pre-built with the SDK — both available via `twenty-sdk/clients`)
|
||||
yarn twenty app:dev
|
||||
|
||||
# Watch your application's function logs
|
||||
yarn twenty logs
|
||||
yarn twenty function:logs
|
||||
|
||||
# Execute a function with a JSON payload
|
||||
yarn twenty exec -n my-function -p '{"key": "value"}'
|
||||
yarn twenty function:execute -n my-function -p '{"key": "value"}'
|
||||
|
||||
# Execute the pre-install function
|
||||
yarn twenty exec --preInstall
|
||||
yarn twenty function:execute --preInstall
|
||||
|
||||
# Execute the post-install function
|
||||
yarn twenty exec --postInstall
|
||||
yarn twenty function:execute --postInstall
|
||||
|
||||
# Build the app for distribution
|
||||
yarn twenty build
|
||||
yarn twenty app:build
|
||||
|
||||
# Publish the app to npm or directly to a Twenty server
|
||||
yarn twenty publish
|
||||
yarn twenty app:publish
|
||||
|
||||
# Uninstall the application from the current workspace
|
||||
yarn twenty uninstall
|
||||
yarn twenty app:uninstall
|
||||
```
|
||||
|
||||
## Scaffolding modes
|
||||
@@ -107,35 +107,13 @@ npx create-twenty-app@latest my-app -m
|
||||
- `skills/example-skill.ts` — Example AI agent skill definition
|
||||
- `__tests__/app-install.integration-test.ts` — Integration test that builds, installs, and verifies the app (includes `vitest.config.ts`, `tsconfig.spec.json`, and a setup file)
|
||||
|
||||
## Local server
|
||||
|
||||
The scaffolder can start a local Twenty dev server for you (all-in-one Docker image with PostgreSQL, Redis, server, and worker). You can also manage it manually:
|
||||
|
||||
```bash
|
||||
yarn twenty server start # Start (pulls image if needed)
|
||||
yarn twenty server status # Check if it's healthy
|
||||
yarn twenty server logs # Stream logs
|
||||
yarn twenty server stop # Stop (data is preserved)
|
||||
yarn twenty server reset # Wipe all data and start fresh
|
||||
```
|
||||
|
||||
The server is pre-seeded with a workspace and user (`[email protected]` / `[email protected]`).
|
||||
|
||||
### How to use a local Twenty instance
|
||||
|
||||
If you're already running a local Twenty instance, you can connect to it instead of using Docker. Pass the port your local server is listening on (default: `3000`):
|
||||
|
||||
```bash
|
||||
npx create-twenty-app@latest my-app --port 3000
|
||||
```
|
||||
|
||||
## Next steps
|
||||
|
||||
- Run `yarn twenty help` to see all available commands.
|
||||
- Use `yarn twenty remote add --local` to authenticate with your Twenty workspace via OAuth.
|
||||
- Explore the generated project and add your first entity with `yarn twenty add` (logic functions, front components, objects, roles, views, navigation menu items, skills).
|
||||
- Use `yarn twenty dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
|
||||
- `CoreApiClient` (for workspace data via `/graphql`) is auto-generated by `yarn twenty dev`. `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`) ships pre-built with the SDK. Both are available via `import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/clients'`.
|
||||
- Use `yarn twenty auth:login` to authenticate with your Twenty workspace.
|
||||
- Explore the generated project and add your first entity with `yarn twenty entity:add` (logic functions, front components, objects, roles, views, navigation menu items, skills).
|
||||
- Use `yarn twenty app:dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
|
||||
- `CoreApiClient` (for workspace data via `/graphql`) is auto-generated by `yarn twenty app:dev`. `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`) ships pre-built with the SDK. Both are available via `import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/clients'`.
|
||||
|
||||
## Build and publish your application
|
||||
|
||||
@@ -143,19 +121,19 @@ Once your app is ready, build and publish it using the CLI:
|
||||
|
||||
```bash
|
||||
# Build the app (output goes to .twenty/output/)
|
||||
yarn twenty build
|
||||
yarn twenty app:build
|
||||
|
||||
# Build and create a tarball (.tgz) for distribution
|
||||
yarn twenty build --tarball
|
||||
yarn twenty app:build --tarball
|
||||
|
||||
# Publish to npm (requires npm login)
|
||||
yarn twenty publish
|
||||
yarn twenty app:publish
|
||||
|
||||
# Publish with a dist-tag (e.g. beta, next)
|
||||
yarn twenty publish --tag beta
|
||||
yarn twenty app:publish --tag beta
|
||||
|
||||
# Deploy directly to a Twenty server (builds, uploads, and installs in one step)
|
||||
yarn twenty deploy
|
||||
# Publish directly to a Twenty server (builds, uploads, and installs in one step)
|
||||
yarn twenty app:publish --server https://app.twenty.com
|
||||
```
|
||||
|
||||
### Publish to the Twenty marketplace
|
||||
@@ -175,9 +153,8 @@ Our team reviews contributions for quality, security, and reusability before mer
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Server not starting: check Docker is running (`docker info`), then try `yarn twenty server logs`.
|
||||
- Auth not working: make sure you're logged in to Twenty in the browser first, then run `yarn twenty remote add --local`.
|
||||
- Types not generated: ensure `yarn twenty dev` is running — it auto-generates the typed client.
|
||||
- Auth prompts not appearing: run `yarn twenty auth:login` again and verify the API key permissions.
|
||||
- Types not generated: ensure `yarn twenty app:dev` is running — it auto‑generates the typed client.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-twenty-app",
|
||||
"version": "0.8.0-canary.2",
|
||||
"version": "0.7.0",
|
||||
"description": "Command-line interface to create Twenty application",
|
||||
"main": "dist/cli.cjs",
|
||||
"bin": "dist/cli.cjs",
|
||||
|
||||
@@ -27,14 +27,6 @@ const program = new Command(packageJson.name)
|
||||
'--description <description>',
|
||||
'Application description (skips prompt)',
|
||||
)
|
||||
.option(
|
||||
'--skip-local-instance',
|
||||
'Skip the local Twenty instance setup prompt',
|
||||
)
|
||||
.option(
|
||||
'-p, --port <port>',
|
||||
'Port of an existing Twenty server (skips Docker setup)',
|
||||
)
|
||||
.helpOption('-h, --help', 'Display this help message.')
|
||||
.action(
|
||||
async (
|
||||
@@ -45,8 +37,6 @@ const program = new Command(packageJson.name)
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
skipLocalInstance?: boolean;
|
||||
port?: string;
|
||||
},
|
||||
) => {
|
||||
const modeFlags = [options?.exhaustive, options?.minimal].filter(Boolean);
|
||||
@@ -76,16 +66,12 @@ const program = new Command(packageJson.name)
|
||||
|
||||
const mode: ScaffoldingMode = options?.minimal ? 'minimal' : 'exhaustive';
|
||||
|
||||
const port = options?.port ? parseInt(options.port, 10) : undefined;
|
||||
|
||||
await new CreateAppCommand().execute({
|
||||
directory,
|
||||
mode,
|
||||
name: options?.name,
|
||||
displayName: options?.displayName,
|
||||
description: options?.description,
|
||||
skipLocalInstance: options?.skipLocalInstance,
|
||||
port,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,11 +1,62 @@
|
||||
This is a [Twenty](https://twenty.com) application bootstrapped with [`create-twenty-app`](https://www.npmjs.com/package/create-twenty-app).
|
||||
This is a [Twenty](https://twenty.com) application project bootstrapped with [`create-twenty-app`](https://www.npmjs.com/package/create-twenty-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
Run `yarn twenty help` to list all available commands.
|
||||
First, authenticate to your workspace:
|
||||
|
||||
```bash
|
||||
yarn twenty auth:login
|
||||
```
|
||||
|
||||
Then, start development mode to sync your app and watch for changes:
|
||||
|
||||
```bash
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
Open your Twenty instance and go to `/settings/applications` section to see the result.
|
||||
|
||||
## Available Commands
|
||||
|
||||
Run `yarn twenty help` to list all available commands. Common commands:
|
||||
|
||||
```bash
|
||||
# Authentication
|
||||
yarn twenty auth:login # Authenticate with Twenty
|
||||
yarn twenty auth:logout # Remove credentials
|
||||
yarn twenty auth:status # Check auth status
|
||||
yarn twenty auth:switch # Switch default workspace
|
||||
yarn twenty auth:list # List all configured workspaces
|
||||
|
||||
# Application
|
||||
yarn twenty app:dev # Start dev mode (watch, build, sync, and auto-generate typed client)
|
||||
yarn twenty entity:add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
|
||||
yarn twenty function:logs # Stream function logs
|
||||
yarn twenty function:execute # Execute a function with JSON payload
|
||||
yarn twenty app:uninstall # Uninstall app from workspace
|
||||
```
|
||||
|
||||
## Integration Tests
|
||||
|
||||
If your project includes the example integration test (`src/__tests__/app-install.integration-test.ts`), you can run it with:
|
||||
|
||||
```bash
|
||||
# Make sure a Twenty server is running at http://localhost:3000
|
||||
yarn test
|
||||
```
|
||||
|
||||
The test builds and installs the app, then verifies it appears in the applications list. Test configuration (API URL and API key) is defined in `vitest.config.ts`.
|
||||
|
||||
## LLMs instructions
|
||||
|
||||
Main docs and pitfalls are available in LLMS.md file.
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Twenty Apps documentation](https://docs.twenty.com/developers/extend/capabilities/apps)
|
||||
- [twenty-sdk CLI reference](https://www.npmjs.com/package/twenty-sdk)
|
||||
- [Discord](https://discord.gg/cx5n4Jzs57)
|
||||
To learn more about Twenty applications, take a look at the following resources:
|
||||
|
||||
- [twenty-sdk](https://www.npmjs.com/package/twenty-sdk) - learn about `twenty-sdk` tool.
|
||||
- [Twenty doc](https://docs.twenty.com/) - Twenty's documentation.
|
||||
- Join our [Discord](https://discord.gg/cx5n4Jzs57)
|
||||
|
||||
You can check out [the Twenty GitHub repository](https://github.com/twentyhq/twenty) - your feedback and contributions are welcome!
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import { basename } from 'path';
|
||||
import { copyBaseApplicationProject } from '@/utils/app-template';
|
||||
import { convertToLabel } from '@/utils/convert-to-label';
|
||||
import { install } from '@/utils/install';
|
||||
import {
|
||||
type LocalInstanceResult,
|
||||
setupLocalInstance,
|
||||
} from '@/utils/setup-local-instance';
|
||||
import { tryGitInit } from '@/utils/try-git-init';
|
||||
import chalk from 'chalk';
|
||||
import * as fs from 'fs-extra';
|
||||
import inquirer from 'inquirer';
|
||||
import kebabCase from 'lodash.kebabcase';
|
||||
import { execSync } from 'node:child_process';
|
||||
import * as path from 'path';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
@@ -28,16 +22,14 @@ type CreateAppOptions = {
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
skipLocalInstance?: boolean;
|
||||
port?: number;
|
||||
};
|
||||
|
||||
export class CreateAppCommand {
|
||||
async execute(options: CreateAppOptions = {}): Promise<void> {
|
||||
const { appName, appDisplayName, appDirectory, appDescription } =
|
||||
await this.getAppInfos(options);
|
||||
|
||||
try {
|
||||
const { appName, appDisplayName, appDirectory, appDescription } =
|
||||
await this.getAppInfos(options);
|
||||
|
||||
const exampleOptions = this.resolveExampleOptions(
|
||||
options.mode ?? 'exhaustive',
|
||||
);
|
||||
@@ -60,20 +52,10 @@ export class CreateAppCommand {
|
||||
|
||||
await tryGitInit(appDirectory);
|
||||
|
||||
let localResult: LocalInstanceResult = { running: false };
|
||||
|
||||
if (!options.skipLocalInstance) {
|
||||
localResult = await setupLocalInstance(appDirectory, options.port);
|
||||
|
||||
if (localResult.running && localResult.serverUrl) {
|
||||
await this.connectToLocal(appDirectory, localResult.serverUrl);
|
||||
}
|
||||
}
|
||||
|
||||
this.logSuccess(appDirectory, localResult);
|
||||
this.logSuccess(appDirectory);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('\nCreate application failed:'),
|
||||
chalk.red('Initialization failed:'),
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
process.exit(1);
|
||||
@@ -191,49 +173,22 @@ export class CreateAppCommand {
|
||||
appDirectory: string;
|
||||
appName: string;
|
||||
}): void {
|
||||
console.log(
|
||||
chalk.blue('\n', 'Creating Twenty Application\n'),
|
||||
chalk.gray(`- Directory: ${appDirectory}\n`, `- Name: ${appName}\n`),
|
||||
);
|
||||
console.log(chalk.blue('🎯 Creating Twenty Application'));
|
||||
console.log(chalk.gray(`📁 Directory: ${appDirectory}`));
|
||||
console.log(chalk.gray(`📝 Name: ${appName}`));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
private async connectToLocal(
|
||||
appDirectory: string,
|
||||
serverUrl: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
execSync(`yarn twenty remote add ${serverUrl} --as local`, {
|
||||
cwd: appDirectory,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
} catch {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Authentication skipped. Run `yarn twenty remote add --local` manually.',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private logSuccess(
|
||||
appDirectory: string,
|
||||
localResult: LocalInstanceResult,
|
||||
): void {
|
||||
const dirName = basename(appDirectory);
|
||||
|
||||
console.log(chalk.blue('\nApplication created. Next steps:'));
|
||||
console.log(chalk.gray(`- cd ${dirName}`));
|
||||
|
||||
if (!localResult.running) {
|
||||
console.log(
|
||||
chalk.gray(
|
||||
'- yarn twenty remote add --local # Authenticate with Twenty',
|
||||
),
|
||||
);
|
||||
}
|
||||
private logSuccess(appDirectory: string): void {
|
||||
const dirName = appDirectory.split('/').reverse()[0] ?? '';
|
||||
|
||||
console.log(chalk.green('✅ Application created!'));
|
||||
console.log('');
|
||||
console.log(chalk.blue('Next steps:'));
|
||||
console.log(chalk.gray(` cd ${dirName}`));
|
||||
console.log(
|
||||
chalk.gray('- yarn twenty dev # Start dev mode'),
|
||||
chalk.gray(' yarn twenty auth:login # Authenticate with Twenty'),
|
||||
);
|
||||
console.log(chalk.gray(' yarn twenty app:dev # Start dev mode'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@ describe('scaffoldIntegrationTest', () => {
|
||||
|
||||
expect(content).toContain('TWENTY_API_KEY');
|
||||
expect(content).not.toContain('TWENTY_TEST_API_KEY');
|
||||
expect(content).toContain('TWENTY_API_URL');
|
||||
expect(content).toContain('setup-test.ts');
|
||||
expect(content).toContain('tsconfig.spec.json');
|
||||
expect(content).toContain('integration-test.ts');
|
||||
|
||||
@@ -474,7 +474,7 @@ const createExampleNavigationMenuItem = async ({
|
||||
const universalIdentifier = v4();
|
||||
|
||||
const content = `import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/example-view';
|
||||
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/example-view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: '${universalIdentifier}',
|
||||
@@ -482,7 +482,6 @@ export default defineNavigationMenuItem({
|
||||
icon: 'IconList',
|
||||
color: 'blue',
|
||||
position: 0,
|
||||
type: 'VIEW',
|
||||
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
`;
|
||||
@@ -610,9 +609,8 @@ const createPackageJson = async ({
|
||||
const devDependencies: Record<string, string> = {
|
||||
typescript: '^5.9.3',
|
||||
'@types/node': '^24.7.2',
|
||||
'@types/react': '^19.0.0',
|
||||
react: '^19.0.0',
|
||||
'react-dom': '^19.0.0',
|
||||
'@types/react': '^18.2.0',
|
||||
react: '^18.2.0',
|
||||
oxlint: '^0.16.0',
|
||||
'twenty-sdk': createTwentyAppPackageJson.version,
|
||||
};
|
||||
|
||||
@@ -5,7 +5,6 @@ import { exec } from 'child_process';
|
||||
const execPromise = promisify(exec);
|
||||
|
||||
export const install = async (root: string) => {
|
||||
console.log(chalk.gray('Installing yarn dependencies...'));
|
||||
try {
|
||||
await execPromise('corepack enable', { cwd: root });
|
||||
} catch (error: any) {
|
||||
@@ -15,6 +14,6 @@ export const install = async (root: string) => {
|
||||
try {
|
||||
await execPromise('yarn install', { cwd: root });
|
||||
} catch (error: any) {
|
||||
console.warn(chalk.yellow('yarn install failed:'), error.stdout);
|
||||
console.error(chalk.red('yarn install failed:'), error.stdout);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
import chalk from 'chalk';
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
const LOCAL_PORTS = [2020, 3000];
|
||||
|
||||
// Minimal health check — the full implementation lives in twenty-sdk
|
||||
const isServerReady = async (port: number): Promise<boolean> => {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
||||
|
||||
try {
|
||||
const response = await fetch(`http://localhost:${port}/healthz`, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
const body = await response.json();
|
||||
|
||||
return body.status === 'ok';
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
|
||||
const detectRunningServer = async (
|
||||
preferredPort?: number,
|
||||
): Promise<number | null> => {
|
||||
const ports = preferredPort ? [preferredPort] : LOCAL_PORTS;
|
||||
|
||||
for (const port of ports) {
|
||||
if (await isServerReady(port)) {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export type LocalInstanceResult = {
|
||||
running: boolean;
|
||||
serverUrl?: string;
|
||||
};
|
||||
|
||||
export const setupLocalInstance = async (
|
||||
appDirectory: string,
|
||||
preferredPort?: number,
|
||||
): Promise<LocalInstanceResult> => {
|
||||
const detectedPort = await detectRunningServer(preferredPort);
|
||||
|
||||
if (detectedPort) {
|
||||
const serverUrl = `http://localhost:${detectedPort}`;
|
||||
|
||||
console.log(chalk.green(`Twenty server detected on ${serverUrl}.\n`));
|
||||
|
||||
return { running: true, serverUrl };
|
||||
}
|
||||
|
||||
if (preferredPort) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`No Twenty server found on port ${preferredPort}.\n` +
|
||||
'Start your server and run `yarn twenty remote add --local` manually.\n',
|
||||
),
|
||||
);
|
||||
|
||||
return { running: false };
|
||||
}
|
||||
|
||||
console.log(chalk.blue('Setting up local Twenty instance...\n'));
|
||||
|
||||
try {
|
||||
execSync('yarn twenty server start', {
|
||||
cwd: appDirectory,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
} catch {
|
||||
return { running: false };
|
||||
}
|
||||
|
||||
console.log(chalk.gray('Waiting for Twenty to be ready...\n'));
|
||||
|
||||
const startTime = Date.now();
|
||||
const timeoutMs = 180 * 1000;
|
||||
|
||||
while (Date.now() - startTime < timeoutMs) {
|
||||
if (await isServerReady(LOCAL_PORTS[0])) {
|
||||
const serverUrl = `http://localhost:${LOCAL_PORTS[0]}`;
|
||||
|
||||
console.log(chalk.green(`Server running on '${serverUrl}'\n`));
|
||||
|
||||
return { running: true, serverUrl };
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Twenty server did not become healthy in time.\n',
|
||||
"Check: 'yarn twenty server logs'\n",
|
||||
),
|
||||
);
|
||||
|
||||
return { running: false };
|
||||
};
|
||||
@@ -45,6 +45,7 @@ export default defineConfig({
|
||||
include: ['src/**/*.integration-test.ts'],
|
||||
setupFiles: ['src/__tests__/setup-test.ts'],
|
||||
env: {
|
||||
TWENTY_API_URL: 'http://localhost:3000',
|
||||
TWENTY_API_KEY:
|
||||
'${SEED_API_KEY}',
|
||||
},
|
||||
@@ -93,7 +94,7 @@ import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { beforeAll } from 'vitest';
|
||||
|
||||
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
|
||||
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:3000';
|
||||
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
|
||||
|
||||
const assertServerIsReachable = async () => {
|
||||
@@ -119,13 +120,12 @@ beforeAll(async () => {
|
||||
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
|
||||
|
||||
const configFile = {
|
||||
remotes: {
|
||||
local: {
|
||||
profiles: {
|
||||
default: {
|
||||
apiUrl: process.env.TWENTY_API_URL,
|
||||
apiKey: process.env.TWENTY_API_KEY,
|
||||
},
|
||||
},
|
||||
defaultRemote: 'local',
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
|
||||
@@ -5,13 +5,13 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
|
||||
First, authenticate to your workspace:
|
||||
|
||||
```bash
|
||||
yarn twenty remote add --local
|
||||
yarn twenty auth:login
|
||||
```
|
||||
|
||||
Then, start development mode to sync your app and watch for changes:
|
||||
|
||||
```bash
|
||||
yarn twenty dev
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
Open your Twenty instance and go to `/settings/applications` section to see the result.
|
||||
@@ -21,19 +21,19 @@ Open your Twenty instance and go to `/settings/applications` section to see the
|
||||
Run `yarn twenty help` to list all available commands. Common commands:
|
||||
|
||||
```bash
|
||||
# Remotes & Authentication
|
||||
yarn twenty remote add --local # Authenticate with Twenty
|
||||
yarn twenty remote status # Check auth status
|
||||
yarn twenty remote switch # Switch default remote
|
||||
yarn twenty remote list # List all configured remotes
|
||||
yarn twenty remote remove <name> # Remove a remote
|
||||
# Authentication
|
||||
yarn twenty auth:login # Authenticate with Twenty
|
||||
yarn twenty auth:logout # Remove credentials
|
||||
yarn twenty auth:status # Check auth status
|
||||
yarn twenty auth:switch # Switch default workspace
|
||||
yarn twenty auth:list # List all configured workspaces
|
||||
|
||||
# Application
|
||||
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
|
||||
yarn twenty add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
|
||||
yarn twenty logs # Stream function logs
|
||||
yarn twenty exec # Execute a function with JSON payload
|
||||
yarn twenty uninstall # Uninstall app from workspace
|
||||
yarn twenty app:dev # Start dev mode (watch, build, sync, and auto-generate typed client)
|
||||
yarn twenty entity:add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
|
||||
yarn twenty function:logs # Stream function logs
|
||||
yarn twenty function:execute # Execute a function with JSON payload
|
||||
yarn twenty app:uninstall # Uninstall app from workspace
|
||||
```
|
||||
|
||||
## LLMs instructions
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
},
|
||||
"packageManager": "[email protected]",
|
||||
"scripts": {
|
||||
"dev": "twenty dev",
|
||||
"exec": "twenty exec",
|
||||
"uninstall": "twenty uninstall",
|
||||
"app:dev": "twenty app:dev",
|
||||
"function:execute": "twenty function:execute",
|
||||
"app:uninstall": "twenty app:uninstall",
|
||||
"lint": "oxlint -c .oxlintrc.json .",
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
|
||||
},
|
||||
|
||||
@@ -9,16 +9,16 @@
|
||||
},
|
||||
"packageManager": "[email protected]",
|
||||
"scripts": {
|
||||
"remote:add": "twenty remote add --local",
|
||||
"remote:status": "twenty remote status",
|
||||
"remote:switch": "twenty remote switch",
|
||||
"remote:list": "twenty remote list",
|
||||
"remote:remove": "twenty remote remove",
|
||||
"dev": "twenty dev",
|
||||
"add": "twenty add",
|
||||
"logs": "twenty logs",
|
||||
"exec": "twenty exec",
|
||||
"uninstall": "twenty uninstall",
|
||||
"auth:login": "twenty auth:login",
|
||||
"auth:logout": "twenty auth:logout",
|
||||
"auth:status": "twenty auth:status",
|
||||
"auth:switch": "twenty auth:switch",
|
||||
"auth:list": "twenty auth:list",
|
||||
"app:dev": "twenty app:dev",
|
||||
"entity:add": "twenty entity:add",
|
||||
"function:logs": "twenty function:logs",
|
||||
"function:execute": "twenty function:execute",
|
||||
"app:uninstall": "twenty app:uninstall",
|
||||
"help": "twenty help",
|
||||
"lint": "oxlint -c .oxlintrc.json .",
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
|
||||
|
||||
@@ -9,16 +9,16 @@
|
||||
},
|
||||
"packageManager": "[email protected]",
|
||||
"scripts": {
|
||||
"remote:add": "twenty remote add --local",
|
||||
"remote:status": "twenty remote status",
|
||||
"remote:switch": "twenty remote switch",
|
||||
"remote:list": "twenty remote list",
|
||||
"remote:remove": "twenty remote remove",
|
||||
"dev": "twenty dev",
|
||||
"add": "twenty add",
|
||||
"logs": "twenty logs",
|
||||
"exec": "twenty exec",
|
||||
"uninstall": "twenty uninstall",
|
||||
"auth:login": "twenty auth:login",
|
||||
"auth:logout": "twenty auth:logout",
|
||||
"auth:status": "twenty auth:status",
|
||||
"auth:switch": "twenty auth:switch",
|
||||
"auth:list": "twenty auth:list",
|
||||
"app:dev": "twenty app:dev",
|
||||
"entity:add": "twenty entity:add",
|
||||
"function:logs": "twenty function:logs",
|
||||
"function:execute": "twenty function:execute",
|
||||
"app:uninstall": "twenty app:uninstall",
|
||||
"help": "twenty help",
|
||||
"lint": "oxlint -c .oxlintrc.json .",
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
|
||||
|
||||
@@ -9,16 +9,16 @@
|
||||
},
|
||||
"packageManager": "[email protected]",
|
||||
"scripts": {
|
||||
"remote:add": "twenty remote add --local",
|
||||
"remote:status": "twenty remote status",
|
||||
"remote:switch": "twenty remote switch",
|
||||
"remote:list": "twenty remote list",
|
||||
"remote:remove": "twenty remote remove",
|
||||
"dev": "twenty dev",
|
||||
"add": "twenty add",
|
||||
"logs": "twenty logs",
|
||||
"exec": "twenty exec",
|
||||
"uninstall": "twenty uninstall",
|
||||
"auth:login": "twenty auth:login",
|
||||
"auth:logout": "twenty auth:logout",
|
||||
"auth:status": "twenty auth:status",
|
||||
"auth:switch": "twenty auth:switch",
|
||||
"auth:list": "twenty auth:list",
|
||||
"app:dev": "twenty app:dev",
|
||||
"entity:add": "twenty entity:add",
|
||||
"function:logs": "twenty function:logs",
|
||||
"function:execute": "twenty function:execute",
|
||||
"app:uninstall": "twenty app:uninstall",
|
||||
"help": "twenty help",
|
||||
"lint": "oxlint -c .oxlintrc.json .",
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
|
||||
import { ALL_POST_CARD_RECIPIENTS_VIEW_ID } from '../views/all-post-card-recipients.view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: 'c1a2b3c4-0003-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
position: 2,
|
||||
type: NavigationMenuItemType.OBJECT,
|
||||
targetObjectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
|
||||
viewUniversalIdentifier: ALL_POST_CARD_RECIPIENTS_VIEW_ID,
|
||||
});
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
|
||||
import { ALL_POST_CARDS_VIEW_ID } from '../views/all-post-cards.view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: 'c1a2b3c4-0001-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
position: 0,
|
||||
type: NavigationMenuItemType.OBJECT,
|
||||
targetObjectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
viewUniversalIdentifier: ALL_POST_CARDS_VIEW_ID,
|
||||
});
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/recipient.object';
|
||||
import { ALL_RECIPIENTS_VIEW_ID } from '../views/all-recipients.view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: 'c1a2b3c4-0002-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
position: 1,
|
||||
type: NavigationMenuItemType.OBJECT,
|
||||
targetObjectUniversalIdentifier: RECIPIENT_UNIVERSAL_IDENTIFIER,
|
||||
viewUniversalIdentifier: ALL_RECIPIENTS_VIEW_ID,
|
||||
});
|
||||
|
||||
@@ -5,13 +5,13 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
|
||||
First, authenticate to your workspace:
|
||||
|
||||
```bash
|
||||
yarn twenty remote add --local
|
||||
yarn twenty auth:login
|
||||
```
|
||||
|
||||
Then, start development mode to sync your app and watch for changes:
|
||||
|
||||
```bash
|
||||
yarn twenty dev
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
Open your Twenty instance and go to `/settings/applications` section to see the result.
|
||||
@@ -21,19 +21,19 @@ Open your Twenty instance and go to `/settings/applications` section to see the
|
||||
Run `yarn twenty help` to list all available commands. Common commands:
|
||||
|
||||
```bash
|
||||
# Remotes & Authentication
|
||||
yarn twenty remote add --local # Authenticate with Twenty
|
||||
yarn twenty remote status # Check auth status
|
||||
yarn twenty remote switch # Switch default remote
|
||||
yarn twenty remote list # List all configured remotes
|
||||
yarn twenty remote remove <name> # Remove a remote
|
||||
# Authentication
|
||||
yarn twenty auth:login # Authenticate with Twenty
|
||||
yarn twenty auth:logout # Remove credentials
|
||||
yarn twenty auth:status # Check auth status
|
||||
yarn twenty auth:switch # Switch default workspace
|
||||
yarn twenty auth:list # List all configured workspaces
|
||||
|
||||
# Application
|
||||
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
|
||||
yarn twenty add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
|
||||
yarn twenty logs # Stream function logs
|
||||
yarn twenty exec # Execute a function with JSON payload
|
||||
yarn twenty uninstall # Uninstall app from workspace
|
||||
yarn twenty app:dev # Start dev mode (watch, build, sync, and auto-generate typed client)
|
||||
yarn twenty entity:add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
|
||||
yarn twenty function:logs # Stream function logs
|
||||
yarn twenty function:execute # Execute a function with JSON payload
|
||||
yarn twenty app:uninstall # Uninstall app from workspace
|
||||
```
|
||||
|
||||
## Integration Tests
|
||||
|
||||
@@ -29,13 +29,12 @@ beforeAll(async () => {
|
||||
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
|
||||
|
||||
const configFile = {
|
||||
remotes: {
|
||||
local: {
|
||||
profiles: {
|
||||
default: {
|
||||
apiUrl: process.env.TWENTY_API_URL,
|
||||
apiKey: process.env.TWENTY_API_KEY,
|
||||
},
|
||||
},
|
||||
defaultRemote: 'local',
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/example-view';
|
||||
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/example-view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: '10f90627-e9c2-44b7-9742-bed77e3d1b17',
|
||||
@@ -8,6 +7,5 @@ export default defineNavigationMenuItem({
|
||||
icon: 'IconList',
|
||||
color: 'blue',
|
||||
position: 0,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
@@ -5,13 +5,13 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
|
||||
First, authenticate to your workspace:
|
||||
|
||||
```bash
|
||||
yarn twenty remote add --local
|
||||
yarn twenty auth:login
|
||||
```
|
||||
|
||||
Then, start development mode to sync your app and watch for changes:
|
||||
|
||||
```bash
|
||||
yarn twenty dev
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
Open your Twenty instance and go to `/settings/applications` section to see the result.
|
||||
@@ -21,19 +21,19 @@ Open your Twenty instance and go to `/settings/applications` section to see the
|
||||
Run `yarn twenty help` to list all available commands. Common commands:
|
||||
|
||||
```bash
|
||||
# Remotes & Authentication
|
||||
yarn twenty remote add --local # Authenticate with Twenty
|
||||
yarn twenty remote status # Check auth status
|
||||
yarn twenty remote switch # Switch default remote
|
||||
yarn twenty remote list # List all configured remotes
|
||||
yarn twenty remote remove <name> # Remove a remote
|
||||
# Authentication
|
||||
yarn twenty auth:login # Authenticate with Twenty
|
||||
yarn twenty auth:logout # Remove credentials
|
||||
yarn twenty auth:status # Check auth status
|
||||
yarn twenty auth:switch # Switch default workspace
|
||||
yarn twenty auth:list # List all configured workspaces
|
||||
|
||||
# Application
|
||||
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
|
||||
yarn twenty add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
|
||||
yarn twenty logs # Stream function logs
|
||||
yarn twenty exec # Execute a function with JSON payload
|
||||
yarn twenty uninstall # Uninstall app from workspace
|
||||
yarn twenty app:dev # Start dev mode (watch, build, sync, and auto-generate typed client)
|
||||
yarn twenty entity:add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
|
||||
yarn twenty function:logs # Stream function logs
|
||||
yarn twenty function:execute # Execute a function with JSON payload
|
||||
yarn twenty app:uninstall # Uninstall app from workspace
|
||||
```
|
||||
|
||||
## LLMs instructions
|
||||
|
||||
@@ -236,7 +236,7 @@ const handler = async (event: any) => {
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: remove `as any` after running `yarn twenty dev` to regenerate the typed client
|
||||
// TODO: remove `as any` after running `yarn twenty app:dev` to regenerate the typed client
|
||||
const updateSummary = async (markdown: string) => {
|
||||
await client.mutation({
|
||||
updateCallRecording: {
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { CALL_RECORDING_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/call-recording-view';
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: '5248a62d-7d2e-43a7-ba45-6e8f61876a71',
|
||||
name: 'Call recordings',
|
||||
icon: 'IconPhone',
|
||||
position: 0,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewUniversalIdentifier: CALL_RECORDING_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers.constant';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
@@ -7,7 +6,6 @@ export default defineNavigationMenuItem({
|
||||
name: 'Self host user',
|
||||
icon: 'IconList',
|
||||
position: 1,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.views.selfHostingUserView.universalIdentifier,
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ DOCKER_NETWORK=twenty_network
|
||||
# =============================================================================
|
||||
|
||||
prod-build:
|
||||
@cd ../.. && docker build --target twenty -f ./packages/twenty-docker/twenty/Dockerfile --platform $(PLATFORM) --tag twenty:$(TAG) . && cd -
|
||||
@cd ../.. && docker build -f ./packages/twenty-docker/twenty/Dockerfile --platform $(PLATFORM) --tag twenty:$(TAG) . && cd -
|
||||
|
||||
prod-run:
|
||||
@docker run -d -p 3000:3000 --name twenty twenty:$(TAG)
|
||||
|
||||
@@ -38,6 +38,7 @@ services:
|
||||
|
||||
# EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:[email protected]}
|
||||
# EMAIL_FROM_NAME: ${EMAIL_FROM_NAME:-"John from YourDomain"}
|
||||
# EMAIL_SYSTEM_ADDRESS: ${EMAIL_SYSTEM_ADDRESS:[email protected]}
|
||||
# EMAIL_DRIVER: ${EMAIL_DRIVER:-smtp}
|
||||
# EMAIL_SMTP_HOST: ${EMAIL_SMTP_HOST:-smtp.gmail.com}
|
||||
# EMAIL_SMTP_PORT: ${EMAIL_SMTP_PORT:-465}
|
||||
@@ -91,6 +92,7 @@ services:
|
||||
|
||||
# EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:[email protected]}
|
||||
# EMAIL_FROM_NAME: ${EMAIL_FROM_NAME:-"John from YourDomain"}
|
||||
# EMAIL_SYSTEM_ADDRESS: ${EMAIL_SYSTEM_ADDRESS:[email protected]}
|
||||
# EMAIL_DRIVER: ${EMAIL_DRIVER:-smtp}
|
||||
# EMAIL_SMTP_HOST: ${EMAIL_SMTP_HOST:-smtp.gmail.com}
|
||||
# EMAIL_SMTP_PORT: ${EMAIL_SMTP_PORT:-465}
|
||||
|
||||
@@ -89,15 +89,6 @@ password
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* Check if using external secret for redis password */}}
|
||||
{{- define "twenty.redis.useExternalSecret" -}}
|
||||
{{- if and (not .Values.redisInternal.enabled) .Values.redis.external.secretName .Values.redis.external.passwordKey -}}
|
||||
true
|
||||
{{- else -}}
|
||||
false
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* Compose Redis URL */}}
|
||||
{{- define "twenty.redisUrl" -}}
|
||||
{{- if .Values.server.env.REDIS_URL -}}
|
||||
@@ -108,14 +99,9 @@ false
|
||||
{{- else -}}
|
||||
{{- $host := .Values.redis.external.host | default "redis" -}}
|
||||
{{- $port := .Values.redis.external.port | default 6379 -}}
|
||||
{{- if or (eq (include "twenty.redis.useExternalSecret" .) "true") (.Values.redis.external.password) -}}
|
||||
{{- $auth := ":$(REDIS_PASSWORD)@" -}}
|
||||
{{- printf "redis://%s%s:%v" $auth $host $port -}}
|
||||
{{- else -}}
|
||||
{{- printf "redis://%s:%v" $host $port -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* Compose Server URL from override, ingress, or service */}}
|
||||
{{- define "twenty.serverUrl" -}}
|
||||
|
||||
@@ -46,12 +46,12 @@ spec:
|
||||
volumeMounts:
|
||||
- name: redis-data
|
||||
mountPath: /data
|
||||
volumes:
|
||||
- name: redis-data
|
||||
{{- if .Values.redisInternal.persistence.enabled }}
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ .Values.redisInternal.persistence.existingClaim | default (printf "%s-redis" (include "twenty.fullname" .)) }}
|
||||
{{- else }}
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
volumes:
|
||||
- name: redis-data
|
||||
{{- if .Values.redisInternal.persistence.enabled }}
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ .Values.redisInternal.persistence.existingClaim | default (printf "%s-redis" (include "twenty.fullname" .)) }}
|
||||
{{- else }}
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -83,16 +83,16 @@ spec:
|
||||
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d postgres -v db="${DBNAME}" -Atc "SELECT 1 FROM pg_database WHERE datname = :'db'" | grep -q 1 || \
|
||||
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d postgres -v db="${DBNAME}" -c 'CREATE DATABASE :"db";'
|
||||
echo "Creating app user ${APP_USER} if it doesn't exist..."
|
||||
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d postgres -v app_user="${APP_USER}" -v app_password="${APP_PASSWORD}" <<'EOSQL'
|
||||
DO
|
||||
$do$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = :'app_user') THEN
|
||||
EXECUTE format('CREATE USER %I WITH PASSWORD %L', :'app_user', :'app_password');
|
||||
END IF;
|
||||
END
|
||||
$do$;
|
||||
EOSQL
|
||||
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d postgres -v app_user="${APP_USER}" -v app_password="${APP_PASSWORD}" <<'EOSQL'
|
||||
DO
|
||||
$do$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = :'app_user') THEN
|
||||
EXECUTE format('CREATE USER %I WITH PASSWORD %L', :'app_user', :'app_password');
|
||||
END IF;
|
||||
END
|
||||
$do$;
|
||||
EOSQL
|
||||
echo "Creating core schema and granting permissions..."
|
||||
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d "${DBNAME}" -v app_user="${APP_USER}" -c 'CREATE SCHEMA IF NOT EXISTS core'
|
||||
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d "${DBNAME}" -v db="${DBNAME}" -v app_user="${APP_USER}" -c 'GRANT ALL PRIVILEGES ON DATABASE :"db" TO :"app_user";'
|
||||
@@ -106,6 +106,31 @@ spec:
|
||||
psql -h {{ include "twenty.fullname" . }}-db -p 5432 -U postgres -d "${DBNAME}" -v app_user="${APP_USER}" -c 'ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO :"app_user";'
|
||||
echo "Database ${DBNAME} is ready."
|
||||
{{- end }}
|
||||
- name: run-migrations
|
||||
{{- $img := include "twenty.server.image" . }}
|
||||
image: {{ include "twenty.image.repository" $img }}:{{ include "twenty.image.tag" $img }}
|
||||
imagePullPolicy: {{ include "twenty.image.pullPolicy" $img }}
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- >-
|
||||
npx -y typeorm migration:run -d dist/database/typeorm/core/core.datasource
|
||||
env:
|
||||
{{- if eq (include "twenty.db.useExternalSecret" .) "true" }}
|
||||
- name: DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "twenty.dbPassword.secretName" . }}
|
||||
key: {{ include "twenty.dbPassword.secretKey" . }}
|
||||
- name: PG_DATABASE_URL
|
||||
value: {{ include "twenty.dbUrl.template" . | quote }}
|
||||
{{- else }}
|
||||
- name: PG_DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "twenty.dbUrl.secretName" . }}
|
||||
key: url
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: server
|
||||
{{- $img := include "twenty.server.image" . }}
|
||||
@@ -129,16 +154,6 @@ spec:
|
||||
name: {{ include "twenty.dbUrl.secretName" . }}
|
||||
key: url
|
||||
{{- end }}
|
||||
{{- if eq (include "twenty.redis.useExternalSecret" .) "true" }}
|
||||
- name: REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Values.redis.external.secretName }}
|
||||
key: {{ .Values.redis.external.passwordKey }}
|
||||
{{- else if .Values.redis.external.password }}
|
||||
- name: REDIS_PASSWORD
|
||||
value: {{ .Values.redis.external.password | quote }}
|
||||
{{- end }}
|
||||
- name: REDIS_URL
|
||||
value: {{ include "twenty.redisUrl" . | quote }}
|
||||
- name: SIGN_IN_PREFILLED
|
||||
@@ -156,10 +171,7 @@ spec:
|
||||
key: accessToken
|
||||
{{- $storageEnv := (include "twenty.storageEnv" .) }}
|
||||
{{- if $storageEnv }}
|
||||
{{- $storageEnv | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.server.extraEnv }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{ $storageEnv | nindent 12 }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http-tcp
|
||||
|
||||
@@ -67,16 +67,6 @@ spec:
|
||||
name: {{ include "twenty.dbUrl.secretName" . }}
|
||||
key: url
|
||||
{{- end }}
|
||||
{{- if eq (include "twenty.redis.useExternalSecret" .) "true" }}
|
||||
- name: REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Values.redis.external.secretName }}
|
||||
key: {{ .Values.redis.external.passwordKey }}
|
||||
{{- else if .Values.redis.external.password }}
|
||||
- name: REDIS_PASSWORD
|
||||
value: {{ .Values.redis.external.password | quote }}
|
||||
{{- end }}
|
||||
- name: REDIS_URL
|
||||
value: {{ include "twenty.redisUrl" . | quote }}
|
||||
- name: STORAGE_TYPE
|
||||
@@ -86,12 +76,9 @@ spec:
|
||||
secretKeyRef:
|
||||
name: {{ include "twenty.secret.tokens.name" . }}
|
||||
key: accessToken
|
||||
{{- with .Values.worker.extraEnv }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- $storageEnv := (include "twenty.storageEnv" .) }}
|
||||
{{- if $storageEnv }}
|
||||
{{- $storageEnv | nindent 12 }}
|
||||
{{ $storageEnv | nindent 12 }}
|
||||
{{- end }}
|
||||
resources:
|
||||
{{- toYaml .Values.worker.resources | nindent 12 }}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
{{- if and .Values.redisInternal.enabled .Values.redisInternal.persistence.enabled (not .Values.redisInternal.persistence.existingClaim) }}
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ include "twenty.fullname" . }}-redis
|
||||
namespace: {{ include "twenty.namespace" . }}
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "twenty.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/component: redis
|
||||
spec:
|
||||
accessModes:
|
||||
{{ toYaml .Values.redisInternal.persistence.accessModes | nindent 4 }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.redisInternal.persistence.size }}
|
||||
{{- if .Values.redisInternal.persistence.storageClass }}
|
||||
storageClassName: {{ .Values.redisInternal.persistence.storageClass }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,58 +0,0 @@
|
||||
suite: env mapping
|
||||
templates:
|
||||
- templates/deployment-server.yaml
|
||||
- templates/deployment-worker.yaml
|
||||
release:
|
||||
name: my-twenty
|
||||
namespace: default
|
||||
tests:
|
||||
- it: renders server extraEnv with plain value
|
||||
template: templates/deployment-server.yaml
|
||||
set:
|
||||
server.extraEnv:
|
||||
- name: FEATURE_X_ENABLED
|
||||
value: "true"
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: FEATURE_X_ENABLED
|
||||
value: "true"
|
||||
|
||||
- it: renders server extraEnv with valueFrom
|
||||
template: templates/deployment-server.yaml
|
||||
set:
|
||||
server.extraEnv:
|
||||
- name: SMTP_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: smtp-creds
|
||||
key: password
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: SMTP_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: smtp-creds
|
||||
key: password
|
||||
|
||||
- it: renders worker extraEnv with valueFrom
|
||||
template: templates/deployment-worker.yaml
|
||||
set:
|
||||
worker.extraEnv:
|
||||
- name: CUSTOM_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: my-secret
|
||||
key: custom
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: CUSTOM_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: my-secret
|
||||
key: custom
|
||||
@@ -1,89 +0,0 @@
|
||||
suite: redis external authentication
|
||||
templates:
|
||||
- templates/deployment-server.yaml
|
||||
- templates/deployment-worker.yaml
|
||||
release:
|
||||
name: my-twenty
|
||||
namespace: default
|
||||
tests:
|
||||
- it: injects REDIS_PASSWORD from external secret into server
|
||||
template: templates/deployment-server.yaml
|
||||
set:
|
||||
redisInternal.enabled: false
|
||||
redis.external.host: redis.example.com
|
||||
redis.external.secretName: redis-creds
|
||||
redis.external.passwordKey: password
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: redis-creds
|
||||
key: password
|
||||
|
||||
- it: injects REDIS_PASSWORD from external secret into worker
|
||||
template: templates/deployment-worker.yaml
|
||||
set:
|
||||
redisInternal.enabled: false
|
||||
redis.external.host: redis.example.com
|
||||
redis.external.secretName: redis-creds
|
||||
redis.external.passwordKey: password
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: redis-creds
|
||||
key: password
|
||||
|
||||
- it: injects plaintext REDIS_PASSWORD when password set directly in server
|
||||
template: templates/deployment-server.yaml
|
||||
set:
|
||||
redisInternal.enabled: false
|
||||
redis.external.host: redis.example.com
|
||||
redis.external.password: "s3cr3t"
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: REDIS_PASSWORD
|
||||
value: "s3cr3t"
|
||||
|
||||
- it: injects plaintext REDIS_PASSWORD when password set directly in worker
|
||||
template: templates/deployment-worker.yaml
|
||||
set:
|
||||
redisInternal.enabled: false
|
||||
redis.external.host: redis.example.com
|
||||
redis.external.password: "s3cr3t"
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: REDIS_PASSWORD
|
||||
value: "s3cr3t"
|
||||
|
||||
- it: does not inject REDIS_PASSWORD into server when using internal redis
|
||||
template: templates/deployment-server.yaml
|
||||
set:
|
||||
redisInternal.enabled: true
|
||||
asserts:
|
||||
- notContains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: REDIS_PASSWORD
|
||||
any: true
|
||||
|
||||
- it: does not inject REDIS_PASSWORD into worker when using internal redis
|
||||
template: templates/deployment-worker.yaml
|
||||
set:
|
||||
redisInternal.enabled: true
|
||||
asserts:
|
||||
- notContains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: REDIS_PASSWORD
|
||||
any: true
|
||||
@@ -105,3 +105,18 @@ tests:
|
||||
path: spec.template.spec.initContainers[?(@.name=="ensure-database-exists")].command[2]
|
||||
pattern: ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES
|
||||
|
||||
# TypeORM Migration Tests
|
||||
# ======================
|
||||
# TypeORM migrations are configured to use the core.datasource which targets the
|
||||
# 'core' schema. This ensures the _typeorm_migrations table and all application
|
||||
# tables use the dedicated core schema.
|
||||
|
||||
- it: migrations run against core datasource
|
||||
template: templates/deployment-server.yaml
|
||||
set:
|
||||
db.enabled: true
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: spec.template.spec.initContainers[?(@.name=="run-migrations")].command[2]
|
||||
pattern: core\.datasource
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ tests:
|
||||
- crm.example.com
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.template.spec.containers[0].env[?(@.name=="SERVER_URL")].value
|
||||
path: spec.template.spec.containers[0].env[0].value
|
||||
value: "https://crm.example.com:443"
|
||||
- it: falls back to service when ingress disabled
|
||||
set:
|
||||
@@ -28,7 +28,7 @@ tests:
|
||||
server.env.SERVER_URL: ""
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: spec.template.spec.containers[0].env[?(@.name=="SERVER_URL")].value
|
||||
path: spec.template.spec.containers[0].env[0].value
|
||||
pattern: ^http://my-twenty-twenty-server\.default\.svc\.cluster\.local:3000$
|
||||
---
|
||||
suite: ingress configuration
|
||||
@@ -66,7 +66,6 @@ tests:
|
||||
set:
|
||||
server.ingress.acme: true
|
||||
asserts:
|
||||
- isSubset:
|
||||
path: metadata.annotations
|
||||
content:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
- equal:
|
||||
path: metadata.annotations[cert-manager.io/cluster-issuer]
|
||||
value: letsencrypt-prod
|
||||
|
||||
@@ -61,27 +61,12 @@
|
||||
"EMAIL_SMTP_NO_TLS": { "type": "boolean" },
|
||||
"EMAIL_FROM_ADDRESS": { "type": "string" },
|
||||
"EMAIL_FROM_NAME": { "type": "string" },
|
||||
"EMAIL_SYSTEM_ADDRESS": { "type": "string" },
|
||||
"IS_EMAIL_VERIFICATION_REQUIRED": { "type": "boolean" },
|
||||
"EMAIL_VERIFICATION_TOKEN_EXPIRES_IN": { "type": "string" },
|
||||
"PASSWORD_RESET_TOKEN_EXPIRES_IN": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"extraEnv": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"value": { "type": "string" },
|
||||
"valueFrom": { "type": "object" }
|
||||
},
|
||||
"required": ["name"],
|
||||
"oneOf": [
|
||||
{ "required": ["value"] },
|
||||
{ "required": ["valueFrom"] }
|
||||
]
|
||||
}
|
||||
},
|
||||
"service": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -157,22 +142,6 @@
|
||||
"STORAGE_TYPE": { "type": "string" },
|
||||
"DISABLE_DB_MIGRATIONS": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"extraEnv": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"value": { "type": "string" },
|
||||
"valueFrom": { "type": "object" }
|
||||
},
|
||||
"required": ["name"],
|
||||
"oneOf": [
|
||||
{ "required": ["value"] },
|
||||
{ "required": ["valueFrom"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -52,14 +52,6 @@ server:
|
||||
SIGN_IN_PREFILLED: "false"
|
||||
ACCESS_TOKEN_EXPIRES_IN: "7d"
|
||||
LOGIN_TOKEN_EXPIRES_IN: "1h"
|
||||
extraEnv: []
|
||||
# - name: EMAIL_DRIVER
|
||||
# value: smtp
|
||||
# - name: SMTP_PASSWORD
|
||||
# valueFrom:
|
||||
# secretKeyRef:
|
||||
# name: smtp-creds
|
||||
# key: password
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
@@ -113,8 +105,6 @@ worker:
|
||||
cpu: 1000m
|
||||
memory: 2048Mi
|
||||
|
||||
extraEnv: []
|
||||
|
||||
# PostgreSQL
|
||||
db:
|
||||
enabled: true
|
||||
@@ -184,6 +174,3 @@ redis:
|
||||
external:
|
||||
host: ""
|
||||
port: 6379
|
||||
password: ""
|
||||
secretName: ""
|
||||
passwordKey: ""
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
oneshot
|
||||
@@ -1 +0,0 @@
|
||||
/bin/sh /etc/s6-overlay/scripts/init-db.sh
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Initialize PostgreSQL data directory if empty
|
||||
if [ ! -f /data/postgres/PG_VERSION ]; then
|
||||
echo "Initializing PostgreSQL data directory..."
|
||||
su-exec postgres initdb -D /data/postgres --auth=trust --encoding=UTF8
|
||||
# Allow local connections without password
|
||||
echo "host all all 127.0.0.1/32 trust" >> /data/postgres/pg_hba.conf
|
||||
echo "host all all ::1/128 trust" >> /data/postgres/pg_hba.conf
|
||||
fi
|
||||
|
||||
exec su-exec postgres postgres -D /data/postgres \
|
||||
-c listen_addresses=localhost \
|
||||
-c unix_socket_directories=/tmp
|
||||
@@ -1 +0,0 @@
|
||||
longrun
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/bin/sh
|
||||
exec redis-server \
|
||||
--dir /data/redis \
|
||||
--maxmemory-policy noeviction \
|
||||
--bind 127.0.0.1 \
|
||||
--protected-mode yes
|
||||
@@ -1 +0,0 @@
|
||||
longrun
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/sh
|
||||
cd /app/packages/twenty-server
|
||||
exec yarn start:prod
|
||||
@@ -1 +0,0 @@
|
||||
longrun
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/sh
|
||||
cd /app/packages/twenty-server
|
||||
exec yarn worker:prod
|
||||
@@ -1 +0,0 @@
|
||||
longrun
|
||||
@@ -1,56 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Wait for PostgreSQL to be ready (timeout after 60s)
|
||||
echo "Waiting for PostgreSQL..."
|
||||
TRIES=0
|
||||
until su-exec postgres pg_isready -h localhost; do
|
||||
TRIES=$((TRIES + 1))
|
||||
if [ "$TRIES" -ge 120 ]; then
|
||||
echo "ERROR: PostgreSQL did not become ready within 60s"
|
||||
exit 1
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
echo "PostgreSQL is ready."
|
||||
|
||||
# Create role if it doesn't exist
|
||||
su-exec postgres psql -h localhost -tc \
|
||||
"SELECT 1 FROM pg_roles WHERE rolname='twenty'" | grep -q 1 \
|
||||
|| su-exec postgres psql -h localhost -c "CREATE ROLE twenty WITH LOGIN PASSWORD 'twenty' SUPERUSER"
|
||||
|
||||
# Create database if it doesn't exist
|
||||
su-exec postgres psql -h localhost -tc \
|
||||
"SELECT 1 FROM pg_database WHERE datname='default'" | grep -q 1 \
|
||||
|| su-exec postgres createdb -h localhost -O twenty default
|
||||
|
||||
# Run Twenty database setup and migrations
|
||||
cd /app/packages/twenty-server
|
||||
|
||||
has_schema=$(PGPASSWORD=twenty psql -h localhost -U twenty -d default -tAc \
|
||||
"SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'core')")
|
||||
|
||||
if [ "$has_schema" = "f" ]; then
|
||||
echo "Database appears to be empty, running initial setup..."
|
||||
NODE_OPTIONS="--max-old-space-size=1500" node ./dist/scripts/setup-db.js
|
||||
fi
|
||||
|
||||
# Always run migrations (idempotent — skips already-applied ones)
|
||||
yarn database:migrate:prod
|
||||
|
||||
yarn command:prod cache:flush
|
||||
yarn command:prod upgrade
|
||||
yarn command:prod cache:flush
|
||||
|
||||
# Only seed on first boot — check if the dev workspace already exists
|
||||
has_workspace=$(PGPASSWORD=twenty psql -h localhost -U twenty -d default -tAc \
|
||||
"SELECT EXISTS (SELECT 1 FROM core.workspace WHERE id = '20202020-1c25-4d02-bf25-6aeccf7ea419')")
|
||||
|
||||
if [ "$has_workspace" = "f" ]; then
|
||||
echo "Seeding app dev data..."
|
||||
yarn command:prod workspace:seed:dev --light || true
|
||||
else
|
||||
echo "Dev workspace already seeded, skipping."
|
||||
fi
|
||||
|
||||
echo "Database initialization complete."
|
||||
@@ -1,11 +1,9 @@
|
||||
# ===========================================================================
|
||||
# Shared build stages (used by both targets)
|
||||
# ===========================================================================
|
||||
|
||||
# Base image for common dependencies
|
||||
FROM node:24-alpine AS common-deps
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy only the necessary files for dependency resolution
|
||||
COPY ./package.json ./yarn.lock ./.yarnrc.yml ./tsconfig.base.json ./nx.json /app/
|
||||
COPY ./.yarn/releases /app/.yarn/releases
|
||||
COPY ./.yarn/patches /app/.yarn/patches
|
||||
@@ -18,38 +16,25 @@ COPY ./packages/twenty-shared/package.json /app/packages/twenty-shared/
|
||||
COPY ./packages/twenty-front/package.json /app/packages/twenty-front/
|
||||
COPY ./packages/twenty-sdk/package.json /app/packages/twenty-sdk/
|
||||
|
||||
# Install all dependencies
|
||||
RUN yarn && yarn cache clean && npx nx reset
|
||||
|
||||
|
||||
# Build the back
|
||||
FROM common-deps AS twenty-server-build
|
||||
|
||||
# Copy sourcecode after installing dependences to accelerate subsequents builds
|
||||
COPY ./packages/twenty-emails /app/packages/twenty-emails
|
||||
COPY ./packages/twenty-shared /app/packages/twenty-shared
|
||||
COPY ./packages/twenty-ui /app/packages/twenty-ui
|
||||
COPY ./packages/twenty-sdk /app/packages/twenty-sdk
|
||||
COPY ./packages/twenty-server /app/packages/twenty-server
|
||||
|
||||
RUN npx nx run twenty-server:lingui:extract && \
|
||||
npx nx run twenty-server:lingui:compile && \
|
||||
npx nx run twenty-emails:lingui:extract && \
|
||||
npx nx run twenty-emails:lingui:compile
|
||||
|
||||
RUN npx nx run twenty-server:build
|
||||
|
||||
# Bundle setup-db script into a standalone JS file so the final image
|
||||
# doesn't need tsx or the TypeScript source tree at runtime.
|
||||
RUN npx esbuild packages/twenty-server/scripts/setup-db.ts \
|
||||
--bundle --platform=node --outfile=packages/twenty-server/dist/scripts/setup-db.js \
|
||||
--external:typeorm --external:dotenv --external:pg
|
||||
|
||||
# Clean server build output (type declarations and compiled tests are not needed at runtime;
|
||||
# source maps are kept because twenty-infra extracts them from the image for Sentry uploads)
|
||||
RUN find /app/packages/twenty-server/dist -name '*.d.ts' -delete \
|
||||
&& rm -rf /app/packages/twenty-server/dist/packages/twenty-server/test
|
||||
|
||||
RUN yarn workspaces focus --production twenty-emails twenty-shared twenty-sdk twenty-server
|
||||
|
||||
|
||||
# Build the front
|
||||
FROM common-deps AS twenty-front-build
|
||||
|
||||
ARG REACT_APP_SERVER_BASE_URL
|
||||
@@ -58,26 +43,18 @@ COPY ./packages/twenty-front /app/packages/twenty-front
|
||||
COPY ./packages/twenty-ui /app/packages/twenty-ui
|
||||
COPY ./packages/twenty-shared /app/packages/twenty-shared
|
||||
COPY ./packages/twenty-sdk /app/packages/twenty-sdk
|
||||
RUN npx nx run twenty-front:lingui:extract && \
|
||||
npx nx run twenty-front:lingui:compile
|
||||
# To skip the memory-intensive frontend build, pre-build on the host:
|
||||
# npx nx build twenty-front
|
||||
# The check below will use packages/twenty-front/build/ if it already exists.
|
||||
RUN if [ -d /app/packages/twenty-front/build ]; then \
|
||||
echo "Using pre-built frontend from host"; \
|
||||
else \
|
||||
NODE_OPTIONS="--max-old-space-size=8192" npx nx build twenty-front; \
|
||||
fi
|
||||
RUN npx nx build twenty-front
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Target: twenty (production)
|
||||
# docker build --target twenty -f packages/twenty-docker/twenty/Dockerfile .
|
||||
# ===========================================================================
|
||||
|
||||
# Final stage: Run the application
|
||||
FROM node:24-alpine AS twenty
|
||||
|
||||
RUN apk add --no-cache curl jq postgresql-client
|
||||
# Used to run healthcheck in docker
|
||||
RUN apk add --no-cache curl jq
|
||||
|
||||
RUN npm install -g tsx
|
||||
|
||||
RUN apk add --no-cache postgresql-client
|
||||
|
||||
COPY ./packages/twenty-docker/twenty/entrypoint.sh /app/entrypoint.sh
|
||||
RUN chmod +x /app/entrypoint.sh
|
||||
@@ -89,135 +66,20 @@ ENV REACT_APP_SERVER_BASE_URL=$REACT_APP_SERVER_BASE_URL
|
||||
ARG APP_VERSION
|
||||
ENV APP_VERSION=$APP_VERSION
|
||||
|
||||
# Workspace root config
|
||||
COPY --chown=1000 --from=twenty-server-build /app/package.json /app/yarn.lock /app/.yarnrc.yml /app/
|
||||
COPY --chown=1000 --from=twenty-server-build /app/tsconfig.base.json /app/nx.json /app/
|
||||
COPY --chown=1000 --from=twenty-server-build /app/.yarn /app/.yarn
|
||||
COPY --chown=1000 --from=twenty-server-build /app/node_modules /app/node_modules
|
||||
|
||||
# Server package (compiled dist + package.json only, no src/)
|
||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-server/package.json /app/packages/twenty-server/
|
||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-server/dist /app/packages/twenty-server/dist
|
||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-server/patches /app/packages/twenty-server/patches
|
||||
|
||||
# Workspace packages (dist + package.json; node_modules symlinks resolve to these)
|
||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-shared/package.json /app/packages/twenty-shared/
|
||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-shared/dist /app/packages/twenty-shared/dist
|
||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-emails/package.json /app/packages/twenty-emails/
|
||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-emails/dist /app/packages/twenty-emails/dist
|
||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-sdk/package.json /app/packages/twenty-sdk/
|
||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-ui/package.json /app/packages/twenty-ui/
|
||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-front/package.json /app/packages/twenty-front/
|
||||
|
||||
# Frontend static build
|
||||
# Copy built applications from previous stages
|
||||
COPY --chown=1000 --from=twenty-server-build /app /app
|
||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-server /app/packages/twenty-server
|
||||
COPY --chown=1000 --from=twenty-front-build /app/packages/twenty-front/build /app/packages/twenty-server/dist/front
|
||||
|
||||
# Set metadata and labels
|
||||
LABEL org.opencontainers.image.source=https://github.com/twentyhq/twenty
|
||||
LABEL org.opencontainers.image.description="Production Twenty image with backend and frontend."
|
||||
LABEL org.opencontainers.image.description="This image provides a consistent and reproducible environment for the backend and frontend, ensuring it deploys faster and runs the same way regardless of the deployment environment."
|
||||
|
||||
RUN mkdir -p /app/.local-storage /app/packages/twenty-server/.local-storage && \
|
||||
chown 1000:1000 /app/.local-storage /app/packages/twenty-server/.local-storage
|
||||
chown -R 1000:1000 /app
|
||||
|
||||
# Use non root user with uid 1000
|
||||
USER 1000
|
||||
|
||||
CMD ["node", "dist/main"]
|
||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Target: twenty-app-dev (all-in-one with Postgres + Redis)
|
||||
# docker build --target twenty-app-dev -f packages/twenty-docker/twenty/Dockerfile .
|
||||
# ===========================================================================
|
||||
|
||||
FROM alpine:3.20 AS s6-fetch
|
||||
ARG S6_OVERLAY_VERSION=3.2.0.2
|
||||
ARG TARGETARCH
|
||||
RUN if [ "$TARGETARCH" = "arm64" ]; then echo "aarch64" > /tmp/s6arch; \
|
||||
else echo "x86_64" > /tmp/s6arch; fi
|
||||
RUN S6_ARCH=$(cat /tmp/s6arch) && \
|
||||
wget -O /tmp/s6-overlay-noarch.tar.xz \
|
||||
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz" && \
|
||||
wget -O /tmp/s6-overlay-noarch.tar.xz.sha256 \
|
||||
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz.sha256" && \
|
||||
wget -O /tmp/s6-overlay-arch.tar.xz \
|
||||
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz" && \
|
||||
wget -O /tmp/s6-overlay-arch.tar.xz.sha256 \
|
||||
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz.sha256" && \
|
||||
cd /tmp && \
|
||||
NOARCH_SUM=$(awk '{print $1}' s6-overlay-noarch.tar.xz.sha256) && \
|
||||
ARCH_SUM=$(awk '{print $1}' s6-overlay-arch.tar.xz.sha256) && \
|
||||
echo "$NOARCH_SUM s6-overlay-noarch.tar.xz" | sha256sum -c - && \
|
||||
echo "$ARCH_SUM s6-overlay-arch.tar.xz" | sha256sum -c -
|
||||
|
||||
FROM node:24-alpine AS twenty-app-dev
|
||||
|
||||
# s6-overlay
|
||||
COPY --from=s6-fetch /tmp/s6-overlay-noarch.tar.xz /tmp/
|
||||
COPY --from=s6-fetch /tmp/s6-overlay-arch.tar.xz /tmp/
|
||||
RUN tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz \
|
||||
&& tar -C / -Jxpf /tmp/s6-overlay-arch.tar.xz \
|
||||
&& rm /tmp/s6-overlay-*.tar.xz
|
||||
|
||||
RUN apk add --no-cache \
|
||||
postgresql16 postgresql16-contrib \
|
||||
redis \
|
||||
curl jq su-exec
|
||||
|
||||
# Workspace root config
|
||||
COPY --from=twenty-server-build /app/package.json /app/yarn.lock /app/.yarnrc.yml /app/
|
||||
COPY --from=twenty-server-build /app/tsconfig.base.json /app/nx.json /app/
|
||||
COPY --from=twenty-server-build /app/.yarn /app/.yarn
|
||||
COPY --from=twenty-server-build /app/node_modules /app/node_modules
|
||||
|
||||
# Server package (compiled dist + package.json only, no src/)
|
||||
COPY --from=twenty-server-build /app/packages/twenty-server/package.json /app/packages/twenty-server/
|
||||
COPY --from=twenty-server-build /app/packages/twenty-server/dist /app/packages/twenty-server/dist
|
||||
COPY --from=twenty-server-build /app/packages/twenty-server/patches /app/packages/twenty-server/patches
|
||||
|
||||
# Workspace packages (dist + package.json; node_modules symlinks resolve to these)
|
||||
COPY --from=twenty-server-build /app/packages/twenty-shared/package.json /app/packages/twenty-shared/
|
||||
COPY --from=twenty-server-build /app/packages/twenty-shared/dist /app/packages/twenty-shared/dist
|
||||
COPY --from=twenty-server-build /app/packages/twenty-emails/package.json /app/packages/twenty-emails/
|
||||
COPY --from=twenty-server-build /app/packages/twenty-emails/dist /app/packages/twenty-emails/dist
|
||||
COPY --from=twenty-server-build /app/packages/twenty-sdk/package.json /app/packages/twenty-sdk/
|
||||
COPY --from=twenty-server-build /app/packages/twenty-ui/package.json /app/packages/twenty-ui/
|
||||
COPY --from=twenty-server-build /app/packages/twenty-front/package.json /app/packages/twenty-front/
|
||||
|
||||
# Frontend static build
|
||||
COPY --from=twenty-front-build /app/packages/twenty-front/build /app/packages/twenty-server/dist/front
|
||||
|
||||
# Source maps are not needed in the dev image (no Sentry)
|
||||
RUN find /app/packages/twenty-server/dist -name '*.js.map' -delete
|
||||
|
||||
# s6 service definitions
|
||||
COPY packages/twenty-docker/twenty-app-dev/rootfs/ /
|
||||
|
||||
RUN mkdir -p /data/postgres /data/redis /app/.local-storage \
|
||||
&& chown -R postgres:postgres /data/postgres \
|
||||
&& chown 1000:1000 /data/redis /app/.local-storage
|
||||
|
||||
ARG REACT_APP_SERVER_BASE_URL
|
||||
ARG APP_VERSION=0.0.0
|
||||
|
||||
ENV S6_KEEP_ENV=1
|
||||
ENV PG_DATABASE_URL=postgres://twenty:twenty@localhost:5432/default \
|
||||
SERVER_URL=http://localhost:2020 \
|
||||
REDIS_URL=redis://localhost:6379 \
|
||||
STORAGE_TYPE=local \
|
||||
APP_SECRET=twenty-app-dev-secret-not-for-production \
|
||||
REACT_APP_SERVER_BASE_URL=$REACT_APP_SERVER_BASE_URL \
|
||||
APP_VERSION=$APP_VERSION \
|
||||
NODE_ENV=development \
|
||||
NODE_PORT=3000 \
|
||||
DISABLE_DB_MIGRATIONS=true \
|
||||
DISABLE_CRON_JOBS_REGISTRATION=true \
|
||||
IS_BILLING_ENABLED=false \
|
||||
SIGN_IN_PREFILLED=true
|
||||
|
||||
EXPOSE 3000
|
||||
VOLUME ["/data/postgres", "/app/.local-storage"]
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/twentyhq/twenty
|
||||
LABEL org.opencontainers.image.description="All-in-one Twenty image for local development and SDK usage. Includes PostgreSQL, Redis, server, and worker."
|
||||
|
||||
ENTRYPOINT ["/init"]
|
||||
|
||||
@@ -13,7 +13,7 @@ setup_and_migrate_db() {
|
||||
has_schema=$(psql -tAc "SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'core')" ${PG_DATABASE_URL})
|
||||
if [ "$has_schema" = "f" ]; then
|
||||
echo "Database appears to be empty, running migrations."
|
||||
NODE_OPTIONS="--max-old-space-size=1500" node ./dist/scripts/setup-db.js
|
||||
NODE_OPTIONS="--max-old-space-size=1500" tsx ./scripts/setup-db.ts
|
||||
yarn database:migrate:prod
|
||||
fi
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ Apps let you extend Twenty with custom objects, fields, logic functions, AI skil
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 24+ and Yarn 4
|
||||
- Docker (for the local Twenty dev server)
|
||||
- A Twenty workspace and an API key (create one at https://app.twenty.com/settings/api-webhooks)
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -37,7 +37,7 @@ yarn twenty app:dev
|
||||
The scaffolder supports two modes for controlling which example files are included:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent)
|
||||
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Minimal: only core files (application-config.ts and default-role.ts)
|
||||
@@ -221,18 +221,6 @@ Then add a `twenty` script:
|
||||
|
||||
Now you can run all commands via `yarn twenty <command>`, e.g. `yarn twenty app:dev`, `yarn twenty help`, etc.
|
||||
|
||||
## How to use a local Twenty instance
|
||||
|
||||
If you're already running a Twenty instance locally (e.g. via `npx nx start twenty-server`), you can connect to it instead of using Docker:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# During scaffolding — skip Docker, connect to your running instance
|
||||
npx create-twenty-app@latest my-app --port 3000
|
||||
|
||||
# Or after scaffolding — add a remote pointing to your instance
|
||||
yarn twenty remote add --local --port 3000
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Authentication errors: run `yarn twenty auth:login` and ensure your API key has the required permissions.
|
||||
|
||||
@@ -20,51 +20,19 @@ Apps let you build and manage Twenty customizations **as code**. Instead of conf
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 24+ and Yarn 4
|
||||
- Docker (for the local Twenty dev server)
|
||||
- A Twenty workspace and an API key (create one at https://app.twenty.com/settings/api-webhooks)
|
||||
|
||||
## Getting Started
|
||||
|
||||
Create a new app using the official scaffolder. It can automatically start a local Twenty instance for you:
|
||||
Create a new app using the official scaffolder, then authenticate and start developing:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Scaffold a new app — the CLI will offer to start a local Twenty server
|
||||
# Scaffold a new app (includes all examples by default)
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
# Start dev mode: automatically syncs local changes to your workspace
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
### Local Server Management
|
||||
|
||||
The SDK includes commands to manage a local Twenty dev server (all-in-one Docker image with PostgreSQL, Redis, server, and worker):
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Start the local server (pulls the image if needed)
|
||||
yarn twenty server start
|
||||
|
||||
# Check server status
|
||||
yarn twenty server status
|
||||
|
||||
# Stream server logs
|
||||
yarn twenty server logs
|
||||
|
||||
# Stop the server
|
||||
yarn twenty server stop
|
||||
|
||||
# Reset all data and start fresh
|
||||
yarn twenty server reset
|
||||
```
|
||||
|
||||
The local server comes pre-seeded with a workspace and user (`[email protected]` / `[email protected]`), so you can start developing immediately without any manual setup.
|
||||
|
||||
### Authentication
|
||||
|
||||
Connect your app to the local server using OAuth:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Authenticate via OAuth (opens browser)
|
||||
yarn twenty remote add --local
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
The scaffolder supports two modes for controlling which example files are included:
|
||||
@@ -77,18 +45,6 @@ npx create-twenty-app@latest my-app
|
||||
npx create-twenty-app@latest my-app --minimal
|
||||
```
|
||||
|
||||
### How to use a local Twenty instance
|
||||
|
||||
If you're already running a local Twenty instance, you can connect to it instead of using Docker. Pass the port your local server is listening on (default: `3000`):
|
||||
|
||||
```bash filename="Terminal"
|
||||
# During scaffolding
|
||||
npx create-twenty-app@latest my-app --port 3000
|
||||
|
||||
# Or after scaffolding
|
||||
yarn twenty remote add --local --port 3000
|
||||
```
|
||||
|
||||
From here you can:
|
||||
|
||||
```bash filename="Terminal"
|
||||
|
||||
@@ -157,9 +157,7 @@ plugins: [
|
||||
|
||||
Run `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = '[email protected]';` in database container to get access to admin panel.
|
||||
|
||||
#### When running workflow, workflow run fails with "Logic function execution is disabled. Set LOGIC_FUNCTION_TYPE to LOCAL or LAMBDA to enable."
|
||||
|
||||
In production, logic functions are disabled by default. Set the `LOGIC_FUNCTION_TYPE` environment variable to `LOCAL` or `LAMBDA` to enable them. This can be configured via environment variables or through the admin panel database variables. See the [Logic Functions setup guide](/developers/self-host/capabilities/setup#logic-functions-available-drivers) for details.
|
||||
|
||||
### 1-click Docker compose
|
||||
|
||||
|
||||
@@ -175,8 +175,7 @@
|
||||
"user-guide/workflows/how-tos/crm-automations/formula-fields",
|
||||
"user-guide/workflows/how-tos/crm-automations/display-related-record-data",
|
||||
"user-guide/workflows/how-tos/crm-automations/closed-won-automations",
|
||||
"user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
|
||||
"user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
|
||||
"user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -622,8 +621,7 @@
|
||||
"l/fr/user-guide/workflows/how-tos/crm-automations/formula-fields",
|
||||
"l/fr/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
|
||||
"l/fr/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
|
||||
"l/fr/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
|
||||
"l/fr/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
|
||||
"l/fr/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1069,8 +1067,7 @@
|
||||
"l/ar/user-guide/workflows/how-tos/crm-automations/formula-fields",
|
||||
"l/ar/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
|
||||
"l/ar/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
|
||||
"l/ar/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
|
||||
"l/ar/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
|
||||
"l/ar/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1516,8 +1513,7 @@
|
||||
"l/cs/user-guide/workflows/how-tos/crm-automations/formula-fields",
|
||||
"l/cs/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
|
||||
"l/cs/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
|
||||
"l/cs/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
|
||||
"l/cs/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
|
||||
"l/cs/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1963,8 +1959,7 @@
|
||||
"l/de/user-guide/workflows/how-tos/crm-automations/formula-fields",
|
||||
"l/de/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
|
||||
"l/de/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
|
||||
"l/de/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
|
||||
"l/de/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
|
||||
"l/de/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -2410,8 +2405,7 @@
|
||||
"l/es/user-guide/workflows/how-tos/crm-automations/formula-fields",
|
||||
"l/es/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
|
||||
"l/es/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
|
||||
"l/es/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
|
||||
"l/es/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
|
||||
"l/es/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -2857,8 +2851,7 @@
|
||||
"l/it/user-guide/workflows/how-tos/crm-automations/formula-fields",
|
||||
"l/it/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
|
||||
"l/it/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
|
||||
"l/it/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
|
||||
"l/it/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
|
||||
"l/it/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -3304,8 +3297,7 @@
|
||||
"l/ja/user-guide/workflows/how-tos/crm-automations/formula-fields",
|
||||
"l/ja/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
|
||||
"l/ja/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
|
||||
"l/ja/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
|
||||
"l/ja/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
|
||||
"l/ja/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -3751,8 +3743,7 @@
|
||||
"l/ko/user-guide/workflows/how-tos/crm-automations/formula-fields",
|
||||
"l/ko/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
|
||||
"l/ko/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
|
||||
"l/ko/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
|
||||
"l/ko/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
|
||||
"l/ko/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -4198,8 +4189,7 @@
|
||||
"l/pt/user-guide/workflows/how-tos/crm-automations/formula-fields",
|
||||
"l/pt/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
|
||||
"l/pt/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
|
||||
"l/pt/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
|
||||
"l/pt/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
|
||||
"l/pt/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -4645,8 +4635,7 @@
|
||||
"l/ro/user-guide/workflows/how-tos/crm-automations/formula-fields",
|
||||
"l/ro/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
|
||||
"l/ro/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
|
||||
"l/ro/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
|
||||
"l/ro/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
|
||||
"l/ro/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -5092,8 +5081,7 @@
|
||||
"l/ru/user-guide/workflows/how-tos/crm-automations/formula-fields",
|
||||
"l/ru/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
|
||||
"l/ru/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
|
||||
"l/ru/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
|
||||
"l/ru/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
|
||||
"l/ru/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -5539,8 +5527,7 @@
|
||||
"l/tr/user-guide/workflows/how-tos/crm-automations/formula-fields",
|
||||
"l/tr/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
|
||||
"l/tr/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
|
||||
"l/tr/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
|
||||
"l/tr/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
|
||||
"l/tr/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -5986,8 +5973,7 @@
|
||||
"l/zh/user-guide/workflows/how-tos/crm-automations/formula-fields",
|
||||
"l/zh/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
|
||||
"l/zh/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
|
||||
"l/zh/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
|
||||
"l/zh/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
|
||||
"l/zh/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
|
Before Width: | Height: | Size: 269 KiB |
|
Before Width: | Height: | Size: 248 KiB |
|
Before Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 931 KiB |
|
Before Width: | Height: | Size: 247 KiB |
|
Before Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 224 KiB |
@@ -20,7 +20,7 @@ description: أنشئ أول تطبيق Twenty خلال دقائق.
|
||||
## المتطلبات الأساسية
|
||||
|
||||
* Node.js 24+ وYarn 4
|
||||
* Docker (لخادم تطوير Twenty المحلي)
|
||||
* مساحة عمل Twenty ومفتاح واجهة برمجة التطبيقات (أنشئ واحدًا على https://app.twenty.com/settings/api-webhooks)
|
||||
|
||||
## البدء
|
||||
|
||||
@@ -38,7 +38,7 @@ yarn twenty app:dev
|
||||
يدعم المُهيئ وضعين للتحكم في ملفات الأمثلة التي سيتم تضمينها:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent)
|
||||
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Minimal: only core files (application-config.ts and default-role.ts)
|
||||
@@ -223,18 +223,6 @@ yarn add -D twenty-sdk
|
||||
|
||||
الآن يمكنك تشغيل جميع الأوامر عبر `yarn twenty <command>`، مثلًا: `yarn twenty app:dev`، `yarn twenty help`، إلخ.
|
||||
|
||||
## كيفية استخدام مثيل محلي من Twenty
|
||||
|
||||
إذا كنت تقوم بتشغيل مثيل محلي من Twenty بالفعل (على سبيل المثال عبر `npx nx start twenty-server`)، فيمكنك الاتصال به بدلًا من استخدام Docker:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# During scaffolding — skip Docker, connect to your running instance
|
||||
npx create-twenty-app@latest my-app --port 3000
|
||||
|
||||
# Or after scaffolding — add a remote pointing to your instance
|
||||
yarn twenty remote add --local --port 3000
|
||||
```
|
||||
|
||||
## استكشاف الأخطاء وإصلاحها
|
||||
|
||||
* أخطاء المصادقة: شغّل `yarn twenty auth:login` وتأكد من أن مفتاح واجهة برمجة التطبيقات لديك يمتلك الأذونات المطلوبة.
|
||||
|
||||
@@ -21,51 +21,19 @@ description: أنشئ وأدِر تخصيصات Twenty على هيئة كود.
|
||||
## المتطلبات الأساسية
|
||||
|
||||
* Node.js 24+ وYarn 4
|
||||
* Docker (لخادم تطوير Twenty المحلي)
|
||||
* مساحة عمل Twenty ومفتاح واجهة برمجة التطبيقات (أنشئ واحدًا على https://app.twenty.com/settings/api-webhooks)
|
||||
|
||||
## البدء
|
||||
|
||||
أنشئ تطبيقًا جديدًا باستخدام المولّد الرسمي. يمكنه بدء مثيل محلي من Twenty تلقائيًا لك:
|
||||
أنشئ تطبيقًا جديدًا باستخدام المُهيئ الرسمي، ثم قم بالمصادقة وابدأ التطوير:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# إنشاء تطبيق جديد — ستعرض واجهة سطر الأوامر خيار بدء خادم Twenty محلي
|
||||
# إنشاء تطبيق جديد (يتضمن جميع الأمثلة افتراضيًا)
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
# ابدأ وضع التطوير: يُزامن التغييرات المحلية تلقائيًا مع مساحة العمل الخاصة بك
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
### إدارة الخادم المحلي
|
||||
|
||||
يتضمن SDK أوامر لإدارة خادم تطوير Twenty محلي (صورة Docker متكاملة تتضمن PostgreSQL وRedis والخادم والعامل):
|
||||
|
||||
```bash filename="Terminal"
|
||||
# ابدأ الخادم المحلي (يسحب الصورة إذا لزم الأمر)
|
||||
yarn twenty server start
|
||||
|
||||
# تحقّق من حالة الخادم
|
||||
yarn twenty server status
|
||||
|
||||
# بثّ سجلات الخادم
|
||||
yarn twenty server logs
|
||||
|
||||
# أوقف الخادم
|
||||
yarn twenty server stop
|
||||
|
||||
# أعد ضبط جميع البيانات وابدأ من جديد
|
||||
yarn twenty server reset
|
||||
```
|
||||
|
||||
يأتي الخادم المحلي مهيأً مسبقًا بمساحة عمل ومستخدم (`[email protected]` / `[email protected]`)، بحيث يمكنك البدء في التطوير فورًا دون أي إعداد يدوي.
|
||||
|
||||
### المصادقة
|
||||
|
||||
وصّل تطبيقك بالخادم المحلي باستخدام OAuth:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# المصادقة عبر OAuth (يفتح المتصفح)
|
||||
yarn twenty remote add --local
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
يدعم المُنشئ وضعين للتحكم في ملفات الأمثلة التي سيتم تضمينها:
|
||||
@@ -78,18 +46,6 @@ npx create-twenty-app@latest my-app
|
||||
npx create-twenty-app@latest my-app --minimal
|
||||
```
|
||||
|
||||
### كيفية استخدام مثيل محلي من Twenty
|
||||
|
||||
إذا كنت تقوم بتشغيل مثيل محلي من Twenty بالفعل، فيمكنك الاتصال به بدلًا من استخدام Docker. مرِّر المنفذ الذي يستمع عليه الخادم المحلي لديك (القيمة الافتراضية: `3000`):
|
||||
|
||||
```bash filename="Terminal"
|
||||
# During scaffolding
|
||||
npx create-twenty-app@latest my-app --port 3000
|
||||
|
||||
# Or after scaffolding
|
||||
yarn twenty remote add --local --port 3000
|
||||
```
|
||||
|
||||
من هنا يمكنك:
|
||||
|
||||
```bash filename="Terminal"
|
||||
|
||||
@@ -166,10 +166,6 @@ plugins: [
|
||||
|
||||
قم بتشغيل `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = '[email protected]';` في حاوية قاعدة البيانات للحصول على الوصول إلى لوحة الإدارة.
|
||||
|
||||
#### عند تشغيل سير العمل، يفشل تشغيل سير العمل مع الرسالة "تم تعطيل تنفيذ دوال المنطق. عيّن LOGIC_FUNCTION_TYPE إلى LOCAL أو LAMBDA لتمكين ذلك."
|
||||
|
||||
في بيئة الإنتاج، يتم تعطيل دوال المنطق افتراضيًا. قم بتعيين متغير البيئة `LOGIC_FUNCTION_TYPE` إلى `LOCAL` أو `LAMBDA` لتمكينها. يمكن تكوين ذلك عبر متغيرات البيئة أو من خلال متغيرات قاعدة البيانات في لوحة الإدارة. راجع [دليل إعداد دوال المنطق](/l/ar/developers/self-host/capabilities/setup#logic-functions-available-drivers) للحصول على التفاصيل.
|
||||
|
||||
### Docker compose بنقرة واحدة
|
||||
|
||||
#### غير قادر على تسجيل الدخول
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: خادم MCP
|
||||
title: خادم MCP},{
|
||||
description: اربط مساعدي الذكاء الاصطناعي بمساحة عمل Twenty الخاصة بك باستخدام بروتوكول سياق النموذج.
|
||||
---
|
||||
|
||||
@@ -87,11 +87,11 @@ MCP حاليًا في مرحلة **ألفا** وهو متاح فقط في بعض
|
||||
| **Cursor** | `.cursor/mcp.json` ضمن مشروعك، أو `~/.cursor/mcp.json` عالميًا |
|
||||
| **ChatGPT** | فعِّل وضع المطوّر في **Settings > Apps & Connectors > Advanced settings**، ثم استخدم **Create** في **Settings > Apps & Connectors** لإضافة خادم MCP |
|
||||
|
||||
### 2. الاتصال
|
||||
### ٢. الاتصال
|
||||
|
||||
أعِد تشغيل عميل MCP لديك (أو أعد تحميل ملف الإعداد). إذا كنت تستخدم OAuth فسيتم توجيهك إلى Twenty لتفويض الوصول. إذا كنت تستخدم مفتاح API فسيكون الاتصال فوريًا.
|
||||
|
||||
### 3. ابدأ باستخدامه
|
||||
### ٣. ابدأ باستخدامه
|
||||
|
||||
اطلب من مساعد الذكاء الاصطناعي التفاعل مع نظام إدارة علاقات العملاء (CRM) لديك:
|
||||
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
---
|
||||
title: الرد التلقائي على رسائل البريد الإلكتروني الواردة
|
||||
description: أنشئ سير عمل يستخدم الذكاء الاصطناعي لفرز رسائل البريد الإلكتروني الواردة وإرسال ردود ضمن سلسلة المحادثة تلقائيًا.
|
||||
---
|
||||
|
||||
استجب لرسائل البريد الإلكتروني الواردة خلال ثوانٍ — وليس ساعات. يستخدم سير العمل هذا وكيلاً للذكاء الاصطناعي لتصفية الضوضاء (النشرات الإخبارية، الرسائل غير المرغوب فيها، الردود التلقائية) وصياغة رد مخصص للرسائل الحقيقية، ثم يرسله كرد مُسلسل داخل المحادثة الأصلية.
|
||||
|
||||
## كيف يعمل تسلسل رسائل البريد الإلكتروني
|
||||
|
||||
تحمل كل رسالة بريد إلكتروني ترويسة `Message-ID` مخفية — بصمة فريدة يعيّنها خادم بريد المرسل. عند الرد على بريد إلكتروني، يقوم عميل البريد لديك بتعيين ترويسة `In-Reply-To` التي تُشير إلى تلك البصمة. بهذه الطريقة يقوم Gmail وOutlook وكل عميل آخر بتجميع الرسائل في سلاسل المحادثات.
|
||||
|
||||
في Twenty، تُخزَّن تلك البصمة كـ `headerMessageId` على كائن Message. يلتقطه سير العمل لديك ويمرّره إلى حقل In-Reply-To لإجراء Send Email.
|
||||
|
||||
## بناء سير العمل
|
||||
|
||||
### الخطوة 1: إنشاء سير عمل جديد
|
||||
|
||||
توجّه إلى **Settings -> Workflows** وانقر **+ New Workflow**.
|
||||
|
||||
### الخطوة 2: التشغيل عند الرسائل الواردة
|
||||
|
||||
اختر **When a Record is Created** ثم **Messages**.
|
||||
|
||||
في كل مرة يصل فيها بريد إلكتروني إلى Twenty، يُشغَّل هذا.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/workflow-overview.png" style={{width:'100%'}} />
|
||||
|
||||
### الخطوة 3: البحث عن المُرسِل
|
||||
|
||||
أضِف إجراء **Search Records**.
|
||||
|
||||
عنوان المُرسِل ليس في الرسالة نفسها — بل في سجل Message Participant المرتبط.
|
||||
|
||||
| الحقل | القيمة |
|
||||
| ---------- | ---------------------------------- |
|
||||
| **الكائن** | المشاركون في الرسالة |
|
||||
| **تصفية** | Message **يساوي** `{{trigger.id}}` |
|
||||
| **تصفية** | Role **يساوي** From |
|
||||
| **الحد** | 1 |
|
||||
|
||||
سيمنحك هذا بريد المُرسِل الإلكتروني في `handle` واسمه في `displayName`.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/find-sender.png" style={{width:'100%'}} />
|
||||
|
||||
### الخطوة 4: الفرز بالذكاء الاصطناعي وصياغة الرد
|
||||
|
||||
أضِف إجراء **AI Agent**. تقوم هذه الخطوة الواحدة بأمرين: تقرر ما إذا كان البريد الإلكتروني يستحق الرد، وإن كان كذلك، تصوغ ردًا.
|
||||
|
||||
استخدم موجهًا مثل:
|
||||
|
||||
```
|
||||
You are an email triage assistant for a sales team. Read the following
|
||||
inbound email and decide if it deserves a reply.
|
||||
|
||||
Subject: {{trigger.subject}}
|
||||
Body: {{trigger.text}}
|
||||
From: {{Find Sender.first.displayName}} ({{Find Sender.first.handle}})
|
||||
|
||||
If this email is spam, a newsletter, an automated notification, or
|
||||
otherwise does not need a human reply, respond with exactly: SKIP
|
||||
|
||||
Otherwise, write a short, professional reply (3-4 sentences max) that:
|
||||
- Acknowledges their specific message
|
||||
- Lets them know someone from the team will follow up shortly
|
||||
- Is warm but not overly casual
|
||||
|
||||
Respond with only the reply text, no subject line or greeting prefix.
|
||||
```
|
||||
|
||||
يُنتِج AI Agent استجابته في الحقل `response` الذي يمكن للخطوات التالية الرجوع إليه.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/ai-triage.png" style={{width:'100%'}} />
|
||||
|
||||
### الخطوة 5: التفريع بناءً على قرار الذكاء الاصطناعي
|
||||
|
||||
أضِف إجراء **If/Else** للتحقق مما إذا كان الذكاء الاصطناعي قرر الرد أم التخطي.
|
||||
|
||||
| الحقل | القيمة |
|
||||
| ------------------ | ------------------------------------------- |
|
||||
| **الشرط** | AI Agent `response` **لا يحتوي على** `SKIP` |
|
||||
| **إذا كان صحيحًا** | المتابعة إلى Send Email |
|
||||
| **وإلا** | لا تفعل شيئًا (ينتهي سير العمل) |
|
||||
|
||||
يتم تجاهل الرسائل غير المرغوب فيها والنشرات البريدية والرسائل المُنشأة تلقائيًا. كل ما عدا ذلك ينتقل إلى الخطوة التالية.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/should-reply.png" style={{width:'100%'}} />
|
||||
|
||||
### الخطوة 6: إرسال رد ضمن سلسلة المحادثة
|
||||
|
||||
أضِف إجراء **Send Email** على فرع "if true". انقر **Advanced options** ثم **Add In-Reply-To**.
|
||||
|
||||
| الحقل | القيمة |
|
||||
| --------------- | -------------------------------------- |
|
||||
| **إلى** | `{{Find Sender.first.handle}}` |
|
||||
| **الموضوع** | `Re: {{trigger.subject}}` |
|
||||
| **المحتوى** | `{{AI Triage & Draft Reply.response}}` |
|
||||
| **In-Reply-To** | `{{trigger.headerMessageId}}` |
|
||||
|
||||
حقل In-Reply-To هو ما يجعل هذا ردًا بدلًا من محادثة جديدة. سيراه المستلِم ضمن سلسلة تحت البريد الأصلي في Gmail أو Outlook أو أي عميل آخر.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/send-email.png" style={{width:'100%'}} />
|
||||
|
||||
<Tip>
|
||||
**In-Reply-To** يتوقّع `message.headerMessageId` من المشغّل — إنها البصمة الفريدة للبريد الإلكتروني، وليست عنوان المستلم. إذا تركته فارغًا، فسيُرسَل البريد الإلكتروني على أي حال، ولكن كرسالة مستقلة.
|
||||
</Tip>
|
||||
|
||||
<Warning>
|
||||
يستخدم Gmail سطر الموضوع لتجميع الرسائل في سلاسل المحادثات. يجب أن يبدأ الموضوع بـ `Re:` (بما في ذلك النقطتان والمسافة) ليعرض Gmail الرد داخل سلسلة المحادثة الأصلية. بدون ذلك، سيظهر الرد كمحادثة منفصلة — حتى إذا تم تعيين ترويسة In-Reply-To بشكل صحيح.
|
||||
</Warning>
|
||||
|
||||
### الخطوة 7: الاختبار والتفعيل
|
||||
|
||||
اضغط **Test**، ثم تحقّق من عميل البريد لديك. يجب أن يظهر الرد متداخلًا تحت الرسالة الأصلية.
|
||||
|
||||
فعِّل عندما تكون راضيًا عنه.
|
||||
|
||||
## أفكار للبناء عليها
|
||||
|
||||
* **الرد على كبار الشخصيات فقط** — أضِف فرعًا يتحقق من مجال المُرسِل أو مما إذا كان موجودًا كجهة اتصال في Twenty
|
||||
* **التوجيه حسب النية** — استخدم موجهات AI Agent منفصلة للتعامل مع استفسارات المبيعات بشكل مختلف عن طلبات الدعم
|
||||
* **الإثراء قبل الرد** — أضِف خطوة Search Records لجلب شركة المُرسِل أو سجل الصفقات إلى الموجه الخاص بالذكاء الاصطناعي للحصول على ردود أكثر تخصيصًا
|
||||
@@ -20,7 +20,7 @@ Apps ermöglichen es Ihnen, Twenty mit benutzerdefinierten Objekten, Feldern, Lo
|
||||
## Voraussetzungen
|
||||
|
||||
* Node.js 24+ und Yarn 4
|
||||
* Docker (für den lokalen Twenty-Dev-Server)
|
||||
* Ein Twenty-Workspace und ein API-Schlüssel (unter https://app.twenty.com/settings/api-webhooks erstellen)
|
||||
|
||||
## Erste Schritte
|
||||
|
||||
@@ -38,7 +38,7 @@ yarn twenty app:dev
|
||||
Das Scaffolding-Tool unterstützt zwei Modi, um zu steuern, welche Beispieldateien enthalten sind:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent)
|
||||
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Minimal: only core files (application-config.ts and default-role.ts)
|
||||
@@ -223,18 +223,6 @@ Fügen Sie dann ein `twenty`-Skript hinzu:
|
||||
|
||||
Jetzt können Sie alle Befehle über `yarn twenty <command>` ausführen, z. B. `yarn twenty app:dev`, `yarn twenty help` usw.
|
||||
|
||||
## So verwenden Sie eine lokale Twenty-Instanz
|
||||
|
||||
Wenn Sie bereits lokal eine Twenty-Instanz ausführen (z. B. über `npx nx start twenty-server`), können Sie sich damit verbinden, anstatt Docker zu verwenden:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# During scaffolding — skip Docker, connect to your running instance
|
||||
npx create-twenty-app@latest my-app --port 3000
|
||||
|
||||
# Or after scaffolding — add a remote pointing to your instance
|
||||
yarn twenty remote add --local --port 3000
|
||||
```
|
||||
|
||||
## Fehlerbehebung
|
||||
|
||||
* Authentifizierungsfehler: Führen Sie `yarn twenty auth:login` aus und stellen Sie sicher, dass Ihr API-Schlüssel die erforderlichen Berechtigungen hat.
|
||||
|
||||
@@ -21,51 +21,19 @@ Mit Apps können Sie Twenty-Anpassungen **als Code** erstellen und verwalten. An
|
||||
## Voraussetzungen
|
||||
|
||||
* Node.js 24+ und Yarn 4
|
||||
* Docker (für den lokalen Twenty-Dev-Server)
|
||||
* Ein Twenty-Workspace und ein API-Schlüssel (unter https://app.twenty.com/settings/api-webhooks erstellen)
|
||||
|
||||
## Erste Schritte
|
||||
|
||||
Erstelle eine neue App mit dem offiziellen Scaffolder. Der Scaffolder kann für dich automatisch eine lokale Twenty-Instanz starten:
|
||||
Erstellen Sie mit dem offiziellen Scaffolder eine neue App, authentifizieren Sie sich und beginnen Sie mit der Entwicklung:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Eine neue App erstellen — die CLI bietet an, einen lokalen Twenty-Server zu starten
|
||||
# Eine neue App erstellen (enthält standardmäßig alle Beispiele)
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
# Dev-Modus starten: synchronisiert lokale Änderungen automatisch mit deinem Arbeitsbereich
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
### Lokale Serververwaltung
|
||||
|
||||
Das SDK enthält Befehle zur Verwaltung eines lokalen Twenty-Dev-Servers (All-in-One-Docker-Image mit PostgreSQL, Redis, Server und Worker):
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Den lokalen Server starten (lädt das Image bei Bedarf herunter)
|
||||
yarn twenty server start
|
||||
|
||||
# Serverstatus prüfen
|
||||
yarn twenty server status
|
||||
|
||||
# Serverprotokolle streamen
|
||||
yarn twenty server logs
|
||||
|
||||
# Server stoppen
|
||||
yarn twenty server stop
|
||||
|
||||
# Alle Daten zurücksetzen und neu starten
|
||||
yarn twenty server reset
|
||||
```
|
||||
|
||||
Der lokale Server ist bereits mit einem Arbeitsbereich und einem Benutzer (`[email protected]` / `[email protected]`) vorbefüllt, sodass Sie ohne manuelle Einrichtung sofort mit der Entwicklung beginnen können.
|
||||
|
||||
### Authentifizierung
|
||||
|
||||
Verbinden Sie Ihre App mithilfe von OAuth mit dem lokalen Server:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Authenticate via OAuth (opens browser)
|
||||
yarn twenty remote add --local
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
Das Scaffolding-Tool unterstützt zwei Modi, um zu steuern, welche Beispieldateien enthalten sind:
|
||||
@@ -78,18 +46,6 @@ npx create-twenty-app@latest my-app
|
||||
npx create-twenty-app@latest my-app --minimal
|
||||
```
|
||||
|
||||
### So verwenden Sie eine lokale Twenty-Instanz
|
||||
|
||||
Wenn Sie bereits eine lokale Twenty-Instanz ausführen, können Sie sich damit verbinden, anstatt Docker zu verwenden. Geben Sie den Port an, auf dem Ihr lokaler Server lauscht (Standard: `3000`):
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Während des Scaffoldings
|
||||
npx create-twenty-app@latest my-app --port 3000
|
||||
|
||||
# Oder nach dem Scaffolding
|
||||
yarn twenty remote add --local --port 3000
|
||||
```
|
||||
|
||||
Von hier aus können Sie:
|
||||
|
||||
```bash filename="Terminal"
|
||||
|
||||
@@ -166,10 +166,6 @@ plugins: [
|
||||
|
||||
Führen Sie den folgenden Befehl im Datenbankcontainer aus, um Zugriff auf das Admin-Panel zu erhalten: `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = '[email protected]';`
|
||||
|
||||
#### Beim Ausführen eines Workflows schlägt die Workflow-Ausführung fehl mit "Logic function execution is disabled. Set LOGIC_FUNCTION_TYPE to LOCAL or LAMBDA to enable."
|
||||
|
||||
In der Produktion sind Logikfunktionen standardmäßig deaktiviert. Setzen Sie die Umgebungsvariable `LOGIC_FUNCTION_TYPE` auf `LOCAL` oder `LAMBDA`, um sie zu aktivieren. Dies kann über Umgebungsvariablen oder über die Datenbankvariablen des Admin-Panels konfiguriert werden. Details finden Sie im [Leitfaden zur Einrichtung von Logikfunktionen](/l/de/developers/self-host/capabilities/setup#logic-functions-available-drivers).
|
||||
|
||||
### 1-Klick Docker Compose
|
||||
|
||||
#### Kann mich nicht einloggen
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
---
|
||||
title: Automatische Antwort auf eingehende E-Mails
|
||||
description: Erstellen Sie einen Workflow, der KI nutzt, um eingehende E-Mails zu sichten und automatisch Antworten im Thread zu senden.
|
||||
---
|
||||
|
||||
Antworten Sie auf eingehende E-Mails in Sekunden — nicht in Stunden. Dieser Workflow verwendet einen KI-Agenten, um Störendes auszufiltern (Newsletter, Spam, automatische Antworten) und eine personalisierte Antwort auf echte Nachrichten zu verfassen und sie dann als Antwort im Thread innerhalb der ursprünglichen Unterhaltung zu senden.
|
||||
|
||||
## Wie E-Mail-Threading funktioniert
|
||||
|
||||
Jede E-Mail enthält einen verborgenen `Message-ID`-Header — einen eindeutigen Fingerabdruck, der vom Mailserver des Absenders zugewiesen wird. Wenn Sie auf eine E-Mail antworten, setzt Ihr E-Mail-Client einen `In-Reply-To`-Header, der auf diesen Fingerabdruck verweist. So gruppieren Gmail, Outlook und alle anderen Clients Nachrichten in Threads.
|
||||
|
||||
In Twenty wird dieser Fingerabdruck als `headerMessageId` am Message-Objekt gespeichert. Ihr Workflow greift ihn auf und übergibt ihn an das Feld In-Reply-To der Aktion Send Email.
|
||||
|
||||
## Den Workflow erstellen
|
||||
|
||||
### Schritt 1: Neuen Workflow erstellen
|
||||
|
||||
Gehen Sie zu **Settings -> Workflows** und klicken Sie auf **+ New Workflow**.
|
||||
|
||||
### Schritt 2: Bei eingehenden Nachrichten auslösen
|
||||
|
||||
Wählen Sie **When a Record is Created** und dann **Messages**.
|
||||
|
||||
Jedes Mal, wenn eine E-Mail in Twenty eingeht, wird dies ausgelöst.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/workflow-overview.png" style={{width:'100%'}} />
|
||||
|
||||
### Schritt 3: Absender ermitteln
|
||||
|
||||
Fügen Sie eine **Datensätze suchen**-Aktion hinzu.
|
||||
|
||||
Die Absenderadresse befindet sich nicht in der Nachricht selbst — sie steht im zugehörigen Datensatz Message Participant.
|
||||
|
||||
| Feld | Wert |
|
||||
| ---------- | -------------------------------- |
|
||||
| **Objekt** | Nachrichtenteilnehmer |
|
||||
| **Filter** | Message **ist** `{{trigger.id}}` |
|
||||
| **Filter** | Rolle **ist** From |
|
||||
| **Limit** | 1 |
|
||||
|
||||
Dadurch erhalten Sie die E-Mail-Adresse des Absenders in `handle` und den Namen in `displayName`.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/find-sender.png" style={{width:'100%'}} />
|
||||
|
||||
### Schritt 4: KI-Triage und Antwortentwurf
|
||||
|
||||
Fügen Sie eine **AI Agent**-Aktion hinzu. Dieser einzelne Schritt erledigt zwei Dinge: Er entscheidet, ob die E-Mail eine Antwort verdient, und falls ja, verfasst er eine.
|
||||
|
||||
Verwenden Sie einen Prompt wie:
|
||||
|
||||
```
|
||||
You are an email triage assistant for a sales team. Read the following
|
||||
inbound email and decide if it deserves a reply.
|
||||
|
||||
Subject: {{trigger.subject}}
|
||||
Body: {{trigger.text}}
|
||||
From: {{Find Sender.first.displayName}} ({{Find Sender.first.handle}})
|
||||
|
||||
If this email is spam, a newsletter, an automated notification, or
|
||||
otherwise does not need a human reply, respond with exactly: SKIP
|
||||
|
||||
Otherwise, write a short, professional reply (3-4 sentences max) that:
|
||||
- Acknowledges their specific message
|
||||
- Lets them know someone from the team will follow up shortly
|
||||
- Is warm but not overly casual
|
||||
|
||||
Respond with only the reply text, no subject line or greeting prefix.
|
||||
```
|
||||
|
||||
Der AI Agent gibt seine Antwort in einem Feld `response` aus, auf das sich die nächsten Schritte beziehen können.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/ai-triage.png" style={{width:'100%'}} />
|
||||
|
||||
### Schritt 5: Verzweigen anhand der KI-Entscheidung
|
||||
|
||||
Fügen Sie eine **If/Else**-Aktion hinzu, um zu prüfen, ob die KI sich entschieden hat zu antworten oder zu überspringen.
|
||||
|
||||
| Feld | Wert |
|
||||
| ------------- | -------------------------------------------- |
|
||||
| **Bedingung** | AI Agent `response` **enthält nicht** `SKIP` |
|
||||
| **Wenn wahr** | Mit Send Email fortfahren |
|
||||
| **Sonst** | Nichts tun (Workflow endet) |
|
||||
|
||||
Spam, Newsletter und automatisch erzeugte Nachrichten werden verworfen. Alles andere geht zum nächsten Schritt über.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/should-reply.png" style={{width:'100%'}} />
|
||||
|
||||
### Schritt 6: Eine Antwort im Thread senden
|
||||
|
||||
Fügen Sie im "Wenn wahr"-Zweig eine **Send Email**-Aktion hinzu. Klicken Sie auf **Advanced options**, dann **Add In-Reply-To**.
|
||||
|
||||
| Feld | Wert |
|
||||
| --------------- | -------------------------------------- |
|
||||
| **An** | `{{Find Sender.first.handle}}` |
|
||||
| **Betreff** | `Re: {{trigger.subject}}` |
|
||||
| **Body** | `{{AI Triage & Draft Reply.response}}` |
|
||||
| **In-Reply-To** | `{{trigger.headerMessageId}}` |
|
||||
|
||||
Das Feld In-Reply-To sorgt dafür, dass dies eine Antwort und keine neue Unterhaltung ist. Der Empfänger sieht sie als Teil des Threads unter der ursprünglichen E-Mail in Gmail, Outlook oder jedem anderen Client.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/send-email.png" style={{width:'100%'}} />
|
||||
|
||||
<Tip>
|
||||
**In-Reply-To** erwartet eine `message.headerMessageId` aus dem Trigger — das ist der eindeutige Fingerabdruck der E-Mail, keine Empfängeradresse. Wenn Sie es leer lassen, wird die E-Mail trotzdem gesendet, nur als eigenständige Nachricht.
|
||||
</Tip>
|
||||
|
||||
<Warning>
|
||||
Gmail verwendet die Betreffzeile, um Nachrichten in Threads zu gruppieren. Der Betreff **muss** mit `Re:` beginnen (einschließlich Doppelpunkt und Leerzeichen), damit Gmail die Antwort im ursprünglichen Thread anzeigt. Ohne dies erscheint die Antwort als separate Unterhaltung — selbst wenn der In-Reply-To-Header korrekt gesetzt ist.
|
||||
</Warning>
|
||||
|
||||
### Schritt 7: Testen und aktivieren
|
||||
|
||||
Klicken Sie auf **Test**, und prüfen Sie dann Ihren E-Mail-Client. Die Antwort sollte unter der ursprünglichen Nachricht verschachtelt erscheinen.
|
||||
|
||||
Aktivieren Sie ihn, wenn Sie zufrieden sind.
|
||||
|
||||
## Ideen zum Ausbau
|
||||
|
||||
* **Nur an VIPs antworten** — fügen Sie einen Zweig hinzu, der die Domain des Absenders prüft oder ob er in Twenty als Kontakt existiert
|
||||
* **Nach Absicht routen** — verwenden Sie separate AI Agent-Prompts, um Vertriebsanfragen anders zu bearbeiten als Supportanfragen
|
||||
* **Vor der Antwort anreichern** — fügen Sie einen Schritt Search Records hinzu, um das Unternehmen oder die Deal-Historie des Absenders in den AI-Prompt zu übernehmen, für persönlichere Antworten
|
||||
@@ -20,7 +20,7 @@ Le app ti permettono di estendere Twenty con oggetti, campi, funzioni logiche, c
|
||||
## Prerequisiti
|
||||
|
||||
* Node.js 24+ e Yarn 4
|
||||
* Docker (per il server di sviluppo locale di Twenty)
|
||||
* Uno spazio di lavoro Twenty e una chiave API (creane una su https://app.twenty.com/settings/api-webhooks)
|
||||
|
||||
## Per iniziare
|
||||
|
||||
@@ -38,7 +38,7 @@ yarn twenty app:dev
|
||||
Lo strumento di scaffolding supporta due modalità per controllare quali file di esempio vengono inclusi:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent)
|
||||
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Minimal: only core files (application-config.ts and default-role.ts)
|
||||
@@ -223,18 +223,6 @@ Quindi aggiungi uno script `twenty`:
|
||||
|
||||
Ora puoi eseguire tutti i comandi tramite `yarn twenty <command>`, ad es. `yarn twenty app:dev`, `yarn twenty help`, ecc.
|
||||
|
||||
## Come utilizzare un'istanza locale di Twenty
|
||||
|
||||
Se stai già eseguendo un'istanza di Twenty in locale (ad es. tramite `npx nx start twenty-server`), puoi connetterti ad essa invece di usare Docker:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# During scaffolding — skip Docker, connect to your running instance
|
||||
npx create-twenty-app@latest my-app --port 3000
|
||||
|
||||
# Or after scaffolding — add a remote pointing to your instance
|
||||
yarn twenty remote add --local --port 3000
|
||||
```
|
||||
|
||||
## Risoluzione dei problemi
|
||||
|
||||
* Errori di autenticazione: esegui `yarn twenty auth:login` e assicurati che la tua chiave API abbia i permessi richiesti.
|
||||
|
||||
@@ -21,51 +21,19 @@ Le app ti consentono di creare e gestire le personalizzazioni di Twenty **come c
|
||||
## Prerequisiti
|
||||
|
||||
* Node.js 24+ e Yarn 4
|
||||
* Docker (per il server di sviluppo locale di Twenty)
|
||||
* Uno spazio di lavoro Twenty e una chiave API (creane una su https://app.twenty.com/settings/api-webhooks)
|
||||
|
||||
## Per iniziare
|
||||
|
||||
Crea una nuova app utilizzando lo scaffolder ufficiale. Può avviare automaticamente un'istanza locale di Twenty per te:
|
||||
Crea una nuova app utilizzando lo scaffolder ufficiale, quindi autenticati e inizia a sviluppare:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Crea lo scaffold di una nuova app — la CLI offrirà di avviare un server locale di Twenty
|
||||
# Crea lo scaffold di una nuova app (include tutti gli esempi per impostazione predefinita)
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
# Avvia la modalità di sviluppo: sincronizza automaticamente le modifiche locali con il tuo workspace
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
### Gestione del server locale
|
||||
|
||||
L'SDK include comandi per gestire un server di sviluppo locale di Twenty (immagine Docker all-in-one con PostgreSQL, Redis, server e worker):
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Avvia il server locale (scarica l'immagine se necessario)
|
||||
yarn twenty server start
|
||||
|
||||
# Verifica lo stato del server
|
||||
yarn twenty server status
|
||||
|
||||
# Segui i log del server
|
||||
yarn twenty server logs
|
||||
|
||||
# Arresta il server
|
||||
yarn twenty server stop
|
||||
|
||||
# Reimposta tutti i dati e riparti da zero
|
||||
yarn twenty server reset
|
||||
```
|
||||
|
||||
Il server locale è preconfigurato con uno spazio di lavoro e un utente (`[email protected]` / `[email protected]`), così puoi iniziare a sviluppare immediatamente senza alcuna configurazione manuale.
|
||||
|
||||
### Autenticazione
|
||||
|
||||
Collega la tua app al server locale tramite OAuth:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Autenticati tramite OAuth (apre il browser)
|
||||
yarn twenty remote add --local
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
Lo strumento di scaffolding supporta due modalità per controllare quali file di esempio vengono inclusi:
|
||||
@@ -78,18 +46,6 @@ npx create-twenty-app@latest my-app
|
||||
npx create-twenty-app@latest my-app --minimal
|
||||
```
|
||||
|
||||
### Come utilizzare un'istanza locale di Twenty
|
||||
|
||||
Se stai già eseguendo un'istanza locale di Twenty, puoi connetterti ad essa invece di usare Docker. Indica la porta su cui il tuo server locale è in ascolto (predefinita: `3000`):
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Durante lo scaffolding
|
||||
npx create-twenty-app@latest my-app --port 3000
|
||||
|
||||
# Oppure dopo lo scaffolding
|
||||
yarn twenty remote add --local --port 3000
|
||||
```
|
||||
|
||||
Da qui puoi:
|
||||
|
||||
```bash filename="Terminal"
|
||||
|
||||
@@ -167,10 +167,6 @@ plugins: [
|
||||
|
||||
Esegui `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = '[email protected]';` nel container del database per accedere al pannello di amministrazione.
|
||||
|
||||
#### Durante l'esecuzione di un workflow, l'esecuzione del workflow non riesce con "L'esecuzione delle funzioni logiche è disabilitata. Imposta LOGIC_FUNCTION_TYPE su LOCAL o LAMBDA per abilitarle."
|
||||
|
||||
In produzione, le funzioni logiche sono disabilitate per impostazione predefinita. Imposta la variabile d'ambiente `LOGIC_FUNCTION_TYPE` su `LOCAL` o `LAMBDA` per abilitarle. Questo può essere configurato tramite variabili d'ambiente o tramite le variabili del database del pannello di amministrazione. Consulta la [guida alla configurazione delle funzioni logiche](/l/it/developers/self-host/capabilities/setup#logic-functions-available-drivers) per dettagli.
|
||||
|
||||
### Composizione Docker a un clic
|
||||
|
||||
#### Impossibile connettersi
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
---
|
||||
title: Risposta automatica alle email in arrivo
|
||||
description: Crea un flusso di lavoro che utilizza l'IA per smistare le email in arrivo e inviare automaticamente risposte in thread.
|
||||
---
|
||||
|
||||
Rispondi alle email in arrivo in pochi secondi — non ore. Questo flusso di lavoro utilizza un agente IA per filtrare il rumore (newsletter, spam, risposte automatiche) e redigere una risposta personalizzata ai messaggi reali, quindi la invia come risposta in thread all'interno della conversazione originale.
|
||||
|
||||
## Come funziona il threading delle email
|
||||
|
||||
Ogni email include un'intestazione nascosta `Message-ID` — un'impronta univoca assegnata dal server di posta del mittente. Quando rispondi a un'email, il tuo client di posta imposta un'intestazione `In-Reply-To` che fa riferimento a quell'impronta. È così che Gmail, Outlook e tutti gli altri client raggruppano i messaggi nei thread.
|
||||
|
||||
In Twenty, quell'impronta è memorizzata come `headerMessageId` sull'oggetto Message. Il tuo flusso di lavoro la recupera e la passa al campo In-Reply-To dell'azione Send Email.
|
||||
|
||||
## Creazione del flusso di lavoro
|
||||
|
||||
### Passaggio 1: Crea un nuovo flusso di lavoro
|
||||
|
||||
Vai a **Impostazioni -> Flussi di lavoro** e fai clic su **+ New Workflow**.
|
||||
|
||||
### Passaggio 2: Imposta il trigger sui messaggi in arrivo
|
||||
|
||||
Scegli **When a Record is Created** e seleziona **Messages**.
|
||||
|
||||
Ogni volta che un'email arriva in Twenty, questo si attiva.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/workflow-overview.png" style={{width:'100%'}} />
|
||||
|
||||
### Passaggio 3: Cerca chi l'ha inviato
|
||||
|
||||
Aggiungi un'azione **Search Records**.
|
||||
|
||||
L'indirizzo del mittente non è presente sul messaggio stesso — si trova nel record Message Participant correlato.
|
||||
|
||||
| Campo | Valore |
|
||||
| ----------- | ------------------------------- |
|
||||
| **Oggetto** | Partecipanti al messaggio |
|
||||
| **Filtro** | Message **is** `{{trigger.id}}` |
|
||||
| **Filtro** | Role **is** From |
|
||||
| **Limite** | 1 |
|
||||
|
||||
Questo ti fornisce l'email del mittente in `handle` e il suo nome in `displayName`.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/find-sender.png" style={{width:'100%'}} />
|
||||
|
||||
### Passaggio 4: Triage con AI e bozza di risposta
|
||||
|
||||
Aggiungi un'azione **AI Agent**. Questo singolo passaggio fa due cose: decide se l'email merita una risposta e, in tal caso, ne scrive una.
|
||||
|
||||
Usa un prompt come:
|
||||
|
||||
```
|
||||
You are an email triage assistant for a sales team. Read the following
|
||||
inbound email and decide if it deserves a reply.
|
||||
|
||||
Subject: {{trigger.subject}}
|
||||
Body: {{trigger.text}}
|
||||
From: {{Find Sender.first.displayName}} ({{Find Sender.first.handle}})
|
||||
|
||||
If this email is spam, a newsletter, an automated notification, or
|
||||
otherwise does not need a human reply, respond with exactly: SKIP
|
||||
|
||||
Otherwise, write a short, professional reply (3-4 sentences max) that:
|
||||
- Acknowledges their specific message
|
||||
- Lets them know someone from the team will follow up shortly
|
||||
- Is warm but not overly casual
|
||||
|
||||
Respond with only the reply text, no subject line or greeting prefix.
|
||||
```
|
||||
|
||||
AI Agent produce la sua risposta in un campo `response` a cui i passaggi successivi possono fare riferimento.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/ai-triage.png" style={{width:'100%'}} />
|
||||
|
||||
### Passaggio 5: Ramifica in base alla decisione dell'AI
|
||||
|
||||
Aggiungi un'azione **If/Else** per verificare se l'AI ha deciso di rispondere o saltare.
|
||||
|
||||
| Campo | Valore |
|
||||
| -------------- | ----------------------------------------------- |
|
||||
| **Condizione** | AI Agent `response` **does not contain** `SKIP` |
|
||||
| **Se vero** | Continua con Send Email |
|
||||
| **Altrimenti** | Non fare nulla (il flusso di lavoro termina) |
|
||||
|
||||
Spam, newsletter e messaggi generati automaticamente vengono scartati. Tutto il resto passa al passaggio successivo.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/should-reply.png" style={{width:'100%'}} />
|
||||
|
||||
### Passaggio 6: Invia una risposta nello stesso thread
|
||||
|
||||
Aggiungi un'azione **Send Email** nel ramo "if true". Fai clic su **Advanced options**, quindi su **Add In-Reply-To**.
|
||||
|
||||
| Campo | Valore |
|
||||
| --------------- | -------------------------------------- |
|
||||
| **A** | `{{Find Sender.first.handle}}` |
|
||||
| **Oggetto** | `Re: {{trigger.subject}}` |
|
||||
| **Corpo** | `{{AI Triage & Draft Reply.response}}` |
|
||||
| **In-Reply-To** | `{{trigger.headerMessageId}}` |
|
||||
|
||||
Il campo In-Reply-To è ciò che rende questo un messaggio di risposta invece di una nuova conversazione. Il destinatario lo vede nel thread sotto l'email originale in Gmail, Outlook o qualsiasi altro client.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/send-email.png" style={{width:'100%'}} />
|
||||
|
||||
<Tip>
|
||||
**In-Reply-To** si aspetta un `message.headerMessageId` dal trigger — è l'impronta univoca dell'email, non un indirizzo del destinatario. Se lo lasci vuoto, l'email viene comunque inviata, solo come messaggio autonomo.
|
||||
</Tip>
|
||||
|
||||
<Warning>
|
||||
Gmail usa l'oggetto per raggruppare i messaggi nei thread. L'oggetto **deve** iniziare con `Re:` (inclusi i due punti e lo spazio) affinché Gmail visualizzi la risposta all'interno del thread originale. Senza di esso, la risposta apparirà come una conversazione separata — anche se l'intestazione In-Reply-To è impostata correttamente.
|
||||
</Warning>
|
||||
|
||||
### Passaggio 7: Testa e attiva
|
||||
|
||||
Fai clic su **Test**, quindi controlla il tuo client di posta. La risposta dovrebbe comparire annidata sotto il messaggio originale.
|
||||
|
||||
Attivalo quando sei soddisfatto.
|
||||
|
||||
## Idee per sviluppare ulteriormente
|
||||
|
||||
* **Rispondi solo ai VIP** — aggiungi un ramo che controlli il dominio del mittente o se esiste come Contatto in Twenty
|
||||
* **Instrada in base all'intento** — usa prompt distinti di AI Agent per gestire le richieste commerciali in modo diverso rispetto a quelle di supporto
|
||||
* **Arricchisci prima di rispondere** — aggiungi un passaggio Search Records per inserire nel prompt dell'AI l'azienda del mittente o la cronologia delle trattative per risposte più personalizzate
|
||||
@@ -20,7 +20,7 @@ Os apps permitem que você estenda o Twenty com objetos, campos, funções de l
|
||||
## Pré-requisitos
|
||||
|
||||
* Node.js 24+ e Yarn 4
|
||||
* Docker (para o servidor de desenvolvimento local do Twenty)
|
||||
* Um espaço de trabalho do Twenty e uma chave de API (crie uma em https://app.twenty.com/settings/api-webhooks)
|
||||
|
||||
## Primeiros passos
|
||||
|
||||
@@ -38,7 +38,7 @@ yarn twenty app:dev
|
||||
O gerador de estrutura oferece suporte a dois modos para controlar quais arquivos de exemplo são incluídos:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent)
|
||||
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Minimal: only core files (application-config.ts and default-role.ts)
|
||||
@@ -223,18 +223,6 @@ Em seguida, adicione um script `twenty`:
|
||||
|
||||
Agora você pode executar todos os comandos via `yarn twenty <command>`, por exemplo, `yarn twenty app:dev`, `yarn twenty help`, etc.
|
||||
|
||||
## Como usar uma instância local do Twenty
|
||||
|
||||
Se você já estiver executando uma instância do Twenty localmente (por exemplo, via `npx nx start twenty-server`), você pode conectar-se a ela em vez de usar o Docker:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# During scaffolding — skip Docker, connect to your running instance
|
||||
npx create-twenty-app@latest my-app --port 3000
|
||||
|
||||
# Or after scaffolding — add a remote pointing to your instance
|
||||
yarn twenty remote add --local --port 3000
|
||||
```
|
||||
|
||||
## Resolução de Problemas
|
||||
|
||||
* Erros de autenticação: execute `yarn twenty auth:login` e certifique-se de que sua chave de API tenha as permissões necessárias.
|
||||
|
||||
@@ -21,51 +21,19 @@ Os aplicativos permitem criar e gerenciar personalizações do Twenty **como có
|
||||
## Pré-requisitos
|
||||
|
||||
* Node.js 24+ e Yarn 4
|
||||
* Docker (para o servidor de desenvolvimento local do Twenty)
|
||||
* Um espaço de trabalho do Twenty e uma chave de API (crie uma em https://app.twenty.com/settings/api-webhooks)
|
||||
|
||||
## Primeiros passos
|
||||
|
||||
Crie um novo app usando o gerador oficial de estrutura. Ele pode iniciar automaticamente uma instância local do Twenty para você:
|
||||
Crie um novo aplicativo usando o gerador oficial, depois autentique-se e comece a desenvolver:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Criar a estrutura de um novo app — a CLI oferecerá iniciar um servidor local do Twenty
|
||||
# Criar a estrutura de um novo app (inclui todos os exemplos por padrão)
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
# Iniciar modo de desenvolvimento: sincroniza automaticamente as alterações locais com seu workspace
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
### Gerenciamento do Servidor Local
|
||||
|
||||
O SDK inclui comandos para gerenciar um servidor de desenvolvimento local do Twenty (imagem Docker all-in-one com PostgreSQL, Redis, servidor e worker):
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Iniciar o servidor local (faz pull da imagem se necessário)
|
||||
yarn twenty server start
|
||||
|
||||
# Verificar o status do servidor
|
||||
yarn twenty server status
|
||||
|
||||
# Transmitir os logs do servidor
|
||||
yarn twenty server logs
|
||||
|
||||
# Parar o servidor
|
||||
yarn twenty server stop
|
||||
|
||||
# Redefinir todos os dados e começar do zero
|
||||
yarn twenty server reset
|
||||
```
|
||||
|
||||
O servidor local já vem pré-configurado com um espaço de trabalho e um usuário (`[email protected]` / `[email protected]`), para que você possa começar a desenvolver imediatamente, sem qualquer configuração manual.
|
||||
|
||||
### Autenticação
|
||||
|
||||
Conecte seu aplicativo ao servidor local usando OAuth:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Authenticate via OAuth (opens browser)
|
||||
yarn twenty remote add --local
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
O gerador de estrutura oferece suporte a dois modos para controlar quais arquivos de exemplo são incluídos:
|
||||
@@ -78,18 +46,6 @@ npx create-twenty-app@latest my-app
|
||||
npx create-twenty-app@latest my-app --minimal
|
||||
```
|
||||
|
||||
### How to use a local Twenty instance
|
||||
|
||||
If you're already running a local Twenty instance, you can connect to it instead of using Docker. Pass the port your local server is listening on (default: `3000`):
|
||||
|
||||
```bash filename="Terminal"
|
||||
# During scaffolding
|
||||
npx create-twenty-app@latest my-app --port 3000
|
||||
|
||||
# Or after scaffolding
|
||||
yarn twenty remote add --local --port 3000
|
||||
```
|
||||
|
||||
A partir daqui você pode:
|
||||
|
||||
```bash filename="Terminal"
|
||||
|
||||