Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ccdaa31273 | |||
| f4f7ccd0e1 |
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git stash:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,63 +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: Build twenty-server
|
||||
run: npx nx build twenty-server
|
||||
|
||||
- name: Run catalog sync
|
||||
run: npx nx run twenty-server:command-no-deps ai:sync-models-dev
|
||||
|
||||
- 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -66,22 +66,6 @@
|
||||
"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 +141,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: ""
|
||||
|
||||
@@ -157,9 +157,7 @@ plugins: [
|
||||
|
||||
Run `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = 'you@yourdomain.com';` in database container to get access to admin panel.
|
||||
|
||||
#### When running workflow, workflow run fails with "Logic function execution is disabled. Set LOGIC_FUNCTION_TYPE to LOCAL or LAMBDA to enable."
|
||||
|
||||
In production, logic functions are disabled by default. Set the `LOGIC_FUNCTION_TYPE` environment variable to `LOCAL` or `LAMBDA` to enable them. This can be configured via environment variables or through the admin panel database variables. See the [Logic Functions setup guide](/developers/self-host/capabilities/setup#logic-functions-available-drivers) for details.
|
||||
|
||||
### 1-click Docker compose
|
||||
|
||||
|
||||
@@ -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 |
@@ -166,10 +166,6 @@ plugins: [
|
||||
|
||||
قم بتشغيل `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = 'you@yourdomain.com';` في حاوية قاعدة البيانات للحصول على الوصول إلى لوحة الإدارة.
|
||||
|
||||
#### عند تشغيل سير العمل، يفشل تشغيل سير العمل مع الرسالة "تم تعطيل تنفيذ دوال المنطق. عيّن LOGIC_FUNCTION_TYPE إلى LOCAL أو LAMBDA لتمكين ذلك."
|
||||
|
||||
في بيئة الإنتاج، يتم تعطيل دوال المنطق افتراضيًا. قم بتعيين متغير البيئة `LOGIC_FUNCTION_TYPE` إلى `LOCAL` أو `LAMBDA` لتمكينها. يمكن تكوين ذلك عبر متغيرات البيئة أو من خلال متغيرات قاعدة البيانات في لوحة الإدارة. راجع [دليل إعداد دوال المنطق](/l/ar/developers/self-host/capabilities/setup#logic-functions-available-drivers) للحصول على التفاصيل.
|
||||
|
||||
### Docker compose بنقرة واحدة
|
||||
|
||||
#### غير قادر على تسجيل الدخول
|
||||
|
||||
@@ -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 لجلب شركة المُرسِل أو سجل الصفقات إلى الموجه الخاص بالذكاء الاصطناعي للحصول على ردود أكثر تخصيصًا
|
||||
@@ -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 = 'you@yourdomain.com';`
|
||||
|
||||
#### 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
|
||||
@@ -167,10 +167,6 @@ plugins: [
|
||||
|
||||
Esegui `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = 'you@yourdomain.com';` 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
|
||||
@@ -166,10 +166,6 @@ plugins: [
|
||||
|
||||
Execute `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = 'você@seudominio.com';` no contêiner de banco de dados para obter acesso ao painel administrativo.
|
||||
|
||||
#### Ao executar um fluxo de trabalho, a execução do fluxo de trabalho falha com "A execução da função de lógica está desativada. Defina LOGIC_FUNCTION_TYPE como LOCAL ou LAMBDA para ativar."
|
||||
|
||||
Em produção, as funções de lógica estão desativadas por padrão. Defina a variável de ambiente `LOGIC_FUNCTION_TYPE` como `LOCAL` ou `LAMBDA` para ativá-las. Isso pode ser configurado por meio de variáveis de ambiente ou pelas variáveis de banco de dados do painel de administração. Veja o [guia de configuração de Funções de Lógica](/l/pt/developers/self-host/capabilities/setup#logic-functions-available-drivers) para obter detalhes.
|
||||
|
||||
### Docker compose com um clique
|
||||
|
||||
#### Impossível efetuar login
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
---
|
||||
title: Resposta automática a e-mails de entrada
|
||||
description: Crie um fluxo de trabalho que use IA para fazer a triagem dos e-mails de entrada e enviar respostas encadeadas automaticamente.
|
||||
---
|
||||
|
||||
Responda a e-mails de entrada em segundos — não em horas. Este fluxo de trabalho usa um Agente de IA para filtrar o ruído (boletins informativos, spam, respostas automáticas) e redigir uma resposta personalizada para mensagens reais e, em seguida, enviá-la como uma resposta encadeada dentro da conversa original.
|
||||
|
||||
## Como funciona o encadeamento de e-mails
|
||||
|
||||
Cada e-mail contém um cabeçalho oculto `Message-ID` — uma impressão digital única atribuída pelo servidor de e-mail do remetente. Quando você responde a um e-mail, seu cliente de e-mail define um cabeçalho `In-Reply-To` que faz referência a essa impressão digital. É assim que o Gmail, o Outlook e todos os outros clientes agrupam as mensagens em conversas.
|
||||
|
||||
No Twenty, essa impressão digital é armazenada como `headerMessageId` no objeto Message. Seu fluxo de trabalho obtém isso e o passa para o campo In-Reply-To da ação Enviar e-mail.
|
||||
|
||||
## Criando o fluxo de trabalho
|
||||
|
||||
### Etapa 1: Criar um novo fluxo de trabalho
|
||||
|
||||
Vá para **Configurações -> Fluxos de trabalho** e clique em **+ Novo fluxo de trabalho**.
|
||||
|
||||
### Etapa 2: Acionar com mensagens recebidas
|
||||
|
||||
Escolha **When a Record is Created** e selecione **Messages**.
|
||||
|
||||
Sempre que um e-mail chega ao Twenty, isto é acionado.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/workflow-overview.png" style={{width:'100%'}} />
|
||||
|
||||
### Etapa 3: Procurar quem enviou
|
||||
|
||||
Adicione uma ação **Pesquisar Registros**.
|
||||
|
||||
O endereço do remetente não está na própria mensagem — está no registro relacionado Message Participant.
|
||||
|
||||
| Campo | Valor |
|
||||
| ---------- | ------------------------------ |
|
||||
| **Objeto** | Participantes da Mensagem |
|
||||
| **Filtro** | Message **é** `{{trigger.id}}` |
|
||||
| **Filtro** | Função **é** From |
|
||||
| **Limite** | 1 |
|
||||
|
||||
Isso fornece o e-mail do remetente em `handle` e o nome dele em `displayName`.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/find-sender.png" style={{width:'100%'}} />
|
||||
|
||||
### Etapa 4: Triagem por IA e rascunho de resposta
|
||||
|
||||
Adicione uma ação **Agente de IA**. Esta única etapa faz duas coisas: decide se o e-mail merece uma resposta e, se sim, redige uma.
|
||||
|
||||
Use um prompt como:
|
||||
|
||||
```
|
||||
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.
|
||||
```
|
||||
|
||||
O Agente de IA produz sua resposta em um campo `response` ao qual as próximas etapas podem se referir.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/ai-triage.png" style={{width:'100%'}} />
|
||||
|
||||
### Etapa 5: Ramificar com base na decisão da IA
|
||||
|
||||
Adicione uma ação **If/Else** para verificar se a IA decidiu responder ou pular.
|
||||
|
||||
| Campo | Valor |
|
||||
| ------------------ | -------------------------------------------------- |
|
||||
| **Condição** | A `response` do Agente de IA **não contém** `SKIP` |
|
||||
| **Se verdadeiro** | Continuar para Enviar e-mail |
|
||||
| **Caso contrário** | Não fazer nada (o fluxo de trabalho termina) |
|
||||
|
||||
Spam, boletins informativos e mensagens geradas automaticamente são descartados. Todo o restante segue para a próxima etapa.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/should-reply.png" style={{width:'100%'}} />
|
||||
|
||||
### Etapa 6: Enviar uma resposta encadeada
|
||||
|
||||
Adicione uma ação **Enviar e-mail** no ramo "se verdadeiro". Clique em **Opções avançadas** e depois em **Adicionar In-Reply-To**.
|
||||
|
||||
| Campo | Valor |
|
||||
| --------------- | -------------------------------------- |
|
||||
| **Para** | `{{Find Sender.first.handle}}` |
|
||||
| **Assunto** | `Re: {{trigger.subject}}` |
|
||||
| **Corpo** | `{{AI Triage & Draft Reply.response}}` |
|
||||
| **In-Reply-To** | `{{trigger.headerMessageId}}` |
|
||||
|
||||
O campo In-Reply-To é o que faz disso uma resposta em vez de uma nova conversa. O destinatário a vê encadeada sob o e-mail original no Gmail, Outlook ou qualquer outro cliente.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/send-email.png" style={{width:'100%'}} />
|
||||
|
||||
<Tip>
|
||||
**In-Reply-To** espera um `message.headerMessageId` do acionador — é a impressão digital única do e-mail, não um endereço do destinatário. Se você deixá-lo vazio, o e-mail ainda será enviado, apenas como uma mensagem independente.
|
||||
</Tip>
|
||||
|
||||
<Warning>
|
||||
O Gmail usa a linha de assunto para agrupar mensagens em conversas. O assunto **deve** começar com `Re:` (incluindo os dois-pontos e o espaço) para que o Gmail exiba a resposta dentro da conversa original. Sem isso, a resposta aparecerá como uma conversa separada — mesmo que o cabeçalho In-Reply-To esteja definido corretamente.
|
||||
</Warning>
|
||||
|
||||
### Etapa 7: Testar e ativar
|
||||
|
||||
Clique em **Test** e, em seguida, verifique seu cliente de e-mail. A resposta deve aparecer aninhada sob a mensagem original.
|
||||
|
||||
Ative quando estiver satisfeito com isso.
|
||||
|
||||
## Ideias para desenvolver
|
||||
|
||||
* **Responder apenas a VIPs** — adicione um ramo que verifique o domínio do remetente ou se ele existe como um Contato no Twenty
|
||||
* **Roteie por intenção** — use prompts separados do Agente de IA para lidar com consultas de vendas de forma diferente de solicitações de suporte
|
||||
* **Enriquecer antes de responder** — adicione uma etapa Pesquisar Registros para trazer a empresa do remetente ou o histórico de negócios para o prompt de IA, obtendo respostas mais personalizadas
|
||||
@@ -167,10 +167,6 @@ plugins: [
|
||||
|
||||
Rulați `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = 'tine@domeniultău.com';` în containerul de baze de date pentru a obține acces la panoul de administrare.
|
||||
|
||||
#### Când rulați un flux de lucru, rularea fluxului de lucru eșuează cu "Execuția funcțiilor logice este dezactivată. Setați LOGIC_FUNCTION_TYPE la LOCAL sau LAMBDA pentru a le activa."
|
||||
|
||||
În producție, funcțiile logice sunt dezactivate în mod implicit. Setați variabila de mediu `LOGIC_FUNCTION_TYPE` la `LOCAL` sau `LAMBDA` pentru a le activa. Acest lucru poate fi configurat prin variabile de mediu sau prin variabilele bazei de date din panoul de administrare. Consultați [Ghidul de configurare a funcțiilor logice](/l/ro/developers/self-host/capabilities/setup#logic-functions-available-drivers) pentru detalii.
|
||||
|
||||
### Docker compose cu un singur click
|
||||
|
||||
#### Nu se poate conecta
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
---
|
||||
title: Răspuns automat la e-mailurile primite
|
||||
description: Creează un flux de lucru care folosește IA pentru a tria e-mailurile primite și pentru a trimite automat răspunsuri în același fir.
|
||||
---
|
||||
|
||||
Răspunde la e-mailurile primite în câteva secunde — nu în ore. Acest flux de lucru folosește un Agent IA pentru a filtra zgomotul (newslettere, spam, răspunsuri automate) și pentru a redacta un răspuns personalizat la mesajele reale, apoi îl trimite ca răspuns în același fir, în cadrul conversației originale.
|
||||
|
||||
## Cum funcționează firele de e-mail
|
||||
|
||||
Fiecare e-mail are un antet ascuns `Message-ID` — o amprentă unică atribuită de serverul de e-mail al expeditorului. Când răspundeți la un e-mail, clientul dvs. de e-mail setează un antet `In-Reply-To` care face referire la acea amprentă. Astfel, Gmail, Outlook și orice alt client grupează mesajele în fire de discuție.
|
||||
|
||||
În Twenty, acea amprentă este stocată ca `headerMessageId` pe obiectul Message. Fluxul dvs. de lucru o preia și o transmite în câmpul In-Reply-To al acțiunii Send Email.
|
||||
|
||||
## Construirea fluxului de lucru
|
||||
|
||||
### Pasul 1: Creați un flux de lucru nou
|
||||
|
||||
Accesați **Settings -> Workflows** și faceți clic pe **+ New Workflow**.
|
||||
|
||||
### Pasul 2: Declanșare la mesajele primite
|
||||
|
||||
Alegeți **When a Record is Created** și selectați **Messages**.
|
||||
|
||||
De fiecare dată când un e-mail ajunge în Twenty, aceasta se declanșează.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/workflow-overview.png" style={{width:'100%'}} />
|
||||
|
||||
### Pasul 3: Căutați cine l-a trimis
|
||||
|
||||
Adăugați o acțiune **Search Records**.
|
||||
|
||||
Adresa expeditorului nu este în mesajul propriu-zis — se găsește în înregistrarea asociată Message Participant.
|
||||
|
||||
| Câmp | Valoare |
|
||||
| ---------- | --------------------------------- |
|
||||
| **Obiect** | Participanți la mesaj |
|
||||
| **Filtru** | Mesajul **este** `{{trigger.id}}` |
|
||||
| **Filtru** | Rolul **este** From |
|
||||
| **Limită** | 1 |
|
||||
|
||||
Aceasta vă oferă e-mailul expeditorului în `handle` și numele acestuia în `displayName`.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/find-sender.png" style={{width:'100%'}} />
|
||||
|
||||
### Pasul 4: Triere cu AI și redactarea răspunsului
|
||||
|
||||
Adăugați o acțiune **AI Agent**. Acest singur pas face două lucruri: decide dacă e-mailul merită un răspuns și, dacă da, redactează unul.
|
||||
|
||||
Folosiți un prompt de tipul:
|
||||
|
||||
```
|
||||
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 răspunsul în câmpul `response`, la care pot face referire pașii următori.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/ai-triage.png" style={{width:'100%'}} />
|
||||
|
||||
### Pasul 5: Ramificați în funcție de decizia AI
|
||||
|
||||
Adăugați o acțiune **If/Else** pentru a verifica dacă AI a decis să răspundă sau să o omită.
|
||||
|
||||
| Câmp | Valoare |
|
||||
| ---------------------- | -------------------------------------------- |
|
||||
| **Condiție** | AI Agent `response` **nu conține** `SKIP` |
|
||||
| **Dacă este adevărat** | Continuați la Send Email |
|
||||
| **Altfel** | Nu faceți nimic (fluxul de lucru se încheie) |
|
||||
|
||||
Spam-ul, buletinele informative și mesajele generate automat sunt ignorate. Tot restul trece la pasul următor.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/should-reply.png" style={{width:'100%'}} />
|
||||
|
||||
### Pasul 6: Trimiteți un răspuns în același fir
|
||||
|
||||
Adăugați o acțiune **Send Email** pe ramura "if true". Faceți clic pe **Advanced options**, apoi pe **Add In-Reply-To**.
|
||||
|
||||
| Câmp | Valoare |
|
||||
| --------------- | -------------------------------------- |
|
||||
| **Către** | `{{Find Sender.first.handle}}` |
|
||||
| **Subiect** | `Re: {{trigger.subject}}` |
|
||||
| **Body** | `{{AI Triage & Draft Reply.response}}` |
|
||||
| **In-Reply-To** | `{{trigger.headerMessageId}}` |
|
||||
|
||||
Câmpul In-Reply-To este cel care face ca acesta să fie un răspuns, nu o conversație nouă. Destinatarul îl vede afișat în fir sub e-mailul original în Gmail, Outlook sau orice alt client.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/send-email.png" style={{width:'100%'}} />
|
||||
|
||||
<Tip>
|
||||
**In-Reply-To** necesită un `message.headerMessageId` din declanșator — este amprenta unică a e-mailului, nu o adresă a destinatarului. Dacă îl lăsați necompletat, e-mailul se trimite în continuare, doar ca un mesaj independent.
|
||||
</Tip>
|
||||
|
||||
<Warning>
|
||||
Gmail folosește subiectul pentru a grupa mesajele în fire de discuție. Subiectul **trebuie** să înceapă cu `Re:` (inclusiv două puncte și spațiu) pentru ca Gmail să afișeze răspunsul în cadrul firului original. Fără acesta, răspunsul va apărea ca o conversație separată — chiar dacă antetul In-Reply-To este setat corect.
|
||||
</Warning>
|
||||
|
||||
### Pasul 7: Testați și activați
|
||||
|
||||
Apăsați **Test**, apoi verificați clientul de e-mail. Răspunsul ar trebui să apară sub mesajul original.
|
||||
|
||||
Activați când sunteți mulțumit(ă) de rezultat.
|
||||
|
||||
## Idei pe care să le dezvoltați
|
||||
|
||||
* **Răspundeți doar VIP-urilor** — adăugați o ramură care verifică domeniul expeditorului sau dacă acesta există ca Contact în Twenty
|
||||
* **Dirijați în funcție de intenție** — folosiți prompturi AI Agent separate pentru a trata diferit solicitările de vânzări față de cererile de asistență
|
||||
* **Îmbogățiți înainte de a răspunde** — adăugați un pas Search Records pentru a include compania expeditorului sau istoricul tranzacțiilor în promptul AI, pentru răspunsuri mai personalizate
|
||||
@@ -167,10 +167,6 @@ plugins: [
|
||||
|
||||
Выполните `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = 'you@yourdomain.com';` в контейнере базы данных, чтобы получить доступ к административной панели.
|
||||
|
||||
#### При запуске рабочего процесса выполнение завершается ошибкой: "Выполнение логической функции отключено. Установите LOGIC_FUNCTION_TYPE в значение LOCAL или LAMBDA, чтобы их включить.
|
||||
|
||||
В производственной среде логические функции по умолчанию отключены. Установите переменную окружения `LOGIC_FUNCTION_TYPE` в `LOCAL` или `LAMBDA`, чтобы их включить. Это можно настроить через переменные окружения или через переменные базы данных в панели администратора. См. [руководство по настройке логических функций](/l/ru/developers/self-host/capabilities/setup#logic-functions-available-drivers) для получения подробной информации.
|
||||
|
||||
### Docker Compose в один клик
|
||||
|
||||
#### Не удается войти в систему
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
---
|
||||
title: Автоответ на входящие письма
|
||||
description: Создайте рабочий процесс, который использует ИИ для сортировки входящих писем и автоматически отправляет ответы в рамках цепочки.
|
||||
---
|
||||
|
||||
Отвечайте на входящие письма за секунды — а не за часы. Этот рабочий процесс использует ИИ-агента, чтобы отфильтровать шум (рассылки, спам, автоответы) и подготовить персонализированный ответ на реальные сообщения, а затем отправляет его как ответ в цепочке внутри исходной переписки.
|
||||
|
||||
## Как работают цепочки писем
|
||||
|
||||
Каждое письмо содержит скрытый заголовок `Message-ID` — уникальный отпечаток, назначаемый почтовым сервером отправителя. Когда вы отвечаете на электронное письмо, ваш почтовый клиент устанавливает заголовок `In-Reply-To`, ссылающийся на этот отпечаток. Именно так Gmail, Outlook и другие клиенты группируют сообщения в цепочки.
|
||||
|
||||
В Twenty этот отпечаток хранится как `headerMessageId` в объекте Message. Ваш рабочий процесс получает его и передает в поле In-Reply-To действия Send Email.
|
||||
|
||||
## Создание рабочего процесса
|
||||
|
||||
### Шаг 1: Создайте новый рабочий процесс
|
||||
|
||||
Перейдите в **Настройки → Рабочие процессы** и нажмите **+ 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** в ветке "Если верно". Нажмите **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**, затем проверьте ваш почтовый клиент. Ответ должен появиться под исходным сообщением.
|
||||
|
||||
Активируйте, когда будете довольны результатом.
|
||||
|
||||
## Идеи для развития
|
||||
|
||||
* **Отвечать только VIP-адресатам** — добавьте ветку, которая проверяет домен отправителя или то, существует ли он как Контакт в Twenty
|
||||
* **Маршрутизация по намерению** — используйте отдельные подсказки для AI Agent, чтобы обрабатывать запросы по продажам иначе, чем обращения в поддержку
|
||||
* **Обогатите данные перед ответом** — добавьте шаг Search Records, чтобы передать компанию отправителя или историю сделок в подсказку ИИ для более персонализированных ответов
|
||||
@@ -166,10 +166,6 @@ plugins: [
|
||||
|
||||
Veritabanı konteynerinde `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = 'you@yourdomain.com';` komutunu çalıştırarak yönetim paneline erişim sağlayın.
|
||||
|
||||
#### Bir iş akışı çalıştırılırken, iş akışının çalıştırılması şu hatayla başarısız oluyor: "Mantık işlevi yürütme devre dışı. Etkinleştirmek için LOGIC_FUNCTION_TYPE değerini LOCAL veya LAMBDA olarak ayarlayın."
|
||||
|
||||
Üretimde, mantık işlevleri varsayılan olarak devre dışıdır. Bunları etkinleştirmek için `LOGIC_FUNCTION_TYPE` ortam değişkenini `LOCAL` veya `LAMBDA` olarak ayarlayın. Bu, ortam değişkenleri aracılığıyla veya yönetici panelindeki veritabanı değişkenleri üzerinden yapılandırılabilir. Ayrıntılar için [Mantık İşlevleri kurulum kılavuzu](/l/tr/developers/self-host/capabilities/setup#logic-functions-available-drivers) bölümüne bakın.
|
||||
|
||||
### 1-tıklama ile Docker Compose
|
||||
|
||||
#### Giriş Yapılamıyor
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
---
|
||||
title: Gelen E-postalara Otomatik Yanıt
|
||||
description: Gelen e-postaları önceliklendirmek için Yapay Zeka (YZ) kullanan ve ileti dizisi içinde yanıtları otomatik olarak gönderen bir iş akışı oluşturun.
|
||||
---
|
||||
|
||||
Gelen e-postalara saatler değil, saniyeler içinde yanıt verin. Bu iş akışı, gereksiz iletileri (bültenler, spam, otomatik yanıtlar) ayıklamak ve gerçek iletiler için kişiselleştirilmiş bir yanıt taslağı oluşturmak üzere bir YZ aracısı kullanır; ardından bunu orijinal konuşmadaki ileti dizisinde bir yanıt olarak gönderir.
|
||||
|
||||
## E-posta İleti Dizisi Nasıl Çalışır
|
||||
|
||||
Her e-posta, göndericinin posta sunucusu tarafından atanan benzersiz bir parmak izi olan gizli bir `Message-ID` üstbilgisi taşır. Bir e-postayı yanıtladığınızda, posta istemciniz o parmak izine referans veren bir `In-Reply-To` başlığı ayarlar. Gmail, Outlook ve diğer tüm istemciler iletileri bu şekilde ileti dizilerine gruplandırır.
|
||||
|
||||
Twenty’de, bu parmak izi Message nesnesinde `headerMessageId` olarak saklanır. İş akışınız bunu alır ve E-posta Gönder eyleminin In-Reply-To alanına aktarır.
|
||||
|
||||
## İş akışını oluşturma
|
||||
|
||||
### Adım 1: Yeni bir İş Akışı oluşturun
|
||||
|
||||
**Ayarlar -> İş Akışları** bölümüne gidin ve **+ Yeni İş Akışı**'na tıklayın.
|
||||
|
||||
### Adım 2: Gelen iletilerde tetikleyin
|
||||
|
||||
**When a Record is Created** seçeneğini seçin ve **Messages**'ı seçin.
|
||||
|
||||
Her e-posta Twenty'ye ulaştığında bu tetiklenir.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/workflow-overview.png" style={{width:'100%'}} />
|
||||
|
||||
### Adım 3: Kimin gönderdiğini bulun
|
||||
|
||||
Bir **Kayıtları Ara** eylemi ekleyin.
|
||||
|
||||
Gönderenin adresi iletinin üzerinde değil — ilgili Message Participant kaydında.
|
||||
|
||||
| Alan | Değer |
|
||||
| ---------- | ------------------------------- |
|
||||
| **Nesne** | Mesaj Katılımcıları |
|
||||
| **Filtre** | Message **is** `{{trigger.id}}` |
|
||||
| **Filtre** | Role **is** From |
|
||||
| **Sınır** | 1 |
|
||||
|
||||
Bu, size gönderenin e-posta adresini `handle` içinde ve adını `displayName` içinde verir.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/find-sender.png" style={{width:'100%'}} />
|
||||
|
||||
### Adım 4: Yapay zekâ triyajı ve yanıt taslağı
|
||||
|
||||
Bir **AI Agent** eylemi ekleyin. Bu tek adım iki şey yapar: e-postanın yanıtı hak edip etmediğine karar verir ve eğer ediyorsa bir yanıt yazar.
|
||||
|
||||
Şuna benzer bir istem kullanın:
|
||||
|
||||
```
|
||||
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, yanıtını sonraki adımların başvurabileceği `response` alanına yazar.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/ai-triage.png" style={{width:'100%'}} />
|
||||
|
||||
### Adım 5: Yapay zekâ kararına göre dallandırın
|
||||
|
||||
Yapay zekânın yanıtlamaya mı yoksa atlamaya mı karar verdiğini kontrol etmek için bir **If/Else** eylemi ekleyin.
|
||||
|
||||
| Alan | Değer |
|
||||
| ----------- | ----------------------------------------------- |
|
||||
| **Koşul** | AI Agent `response` **does not contain** `SKIP` |
|
||||
| **If true** | E-posta Gönder adımına devam edin |
|
||||
| **Else** | Hiçbir şey yapmayın (iş akışı sona erer) |
|
||||
|
||||
Spam, bültenler ve otomatik oluşturulan iletiler atlanır. Diğer her şey bir sonraki adıma geçer.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/should-reply.png" style={{width:'100%'}} />
|
||||
|
||||
### Adım 6: İleti dizisine bağlı bir yanıt gönderin
|
||||
|
||||
"if true" dalına bir **E-posta Gönder** eylemi ekleyin. **Advanced options**'a tıklayın, ardından **Add In-Reply-To**.
|
||||
|
||||
| Alan | Değer |
|
||||
| --------------- | -------------------------------------- |
|
||||
| **Kime** | `{{Find Sender.first.handle}}` |
|
||||
| **Konu** | `Re: {{trigger.subject}}` |
|
||||
| **Gövde** | `{{AI Triage & Draft Reply.response}}` |
|
||||
| **In-Reply-To** | `{{trigger.headerMessageId}}` |
|
||||
|
||||
In-Reply-To alanı, bunun yeni bir konuşma yerine bir yanıt olmasını sağlar. Alıcı, Gmail, Outlook veya diğer herhangi bir istemcide bunu özgün e-postanın altında dizili olarak görür.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/send-email.png" style={{width:'100%'}} />
|
||||
|
||||
<Tip>
|
||||
**In-Reply-To**, tetikleyiciden bir `message.headerMessageId` bekler — bu bir alıcı adresi değil, e-postanın benzersiz parmak izidir. Bunu boş bırakırsanız e-posta yine de gönderilir, yalnızca bağımsız bir ileti olarak.
|
||||
</Tip>
|
||||
|
||||
<Warning>
|
||||
Gmail, iletileri diziler halinde gruplamak için konu satırını kullanır. Gmail’in yanıtı özgün dizinin içinde göstermesi için konu mutlaka `Re:` ile başlamalıdır (iki nokta ve boşluk dahil). Bu olmadan, In-Reply-To başlığı doğru ayarlanmış olsa bile yanıt ayrı bir konuşma olarak görünür.
|
||||
</Warning>
|
||||
|
||||
### Adım 7: Test Edin ve Etkinleştirin
|
||||
|
||||
**Test**'e basın, ardından e-posta istemcinizi kontrol edin. Yanıt, özgün iletinin altında dizili olarak görünmelidir.
|
||||
|
||||
İçinize sindiğinde etkinleştirin.
|
||||
|
||||
## Geliştirmek için fikirler
|
||||
|
||||
* **Yalnızca VIP'lere yanıt verin** — gönderenin alan adını veya Twenty’de bir Contact olarak bulunup bulunmadığını kontrol eden bir dal ekleyin
|
||||
* **Niyete göre yönlendirin** — satış sorgularını destek taleplerinden farklı şekilde ele almak için ayrı AI Agent istemleri kullanın
|
||||
* **Yanıtlamadan önce zenginleştirin** — daha kişiselleştirilmiş yanıtlar için gönderenin şirketini veya anlaşma geçmişini AI istemine çekmek üzere bir Search Records adımı ekleyin
|
||||
@@ -166,10 +166,6 @@ plugins: [
|
||||
|
||||
在数据库容器中运行 `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = 'you@yourdomain.com';` 以访问管理面板。
|
||||
|
||||
#### 在运行工作流时,工作流运行失败,并提示 "Logic function execution is disabled." 将 LOGIC_FUNCTION_TYPE 设置为 LOCAL 或 LAMBDA 以启用。
|
||||
|
||||
在生产环境中,逻辑函数默认处于禁用状态。 将 `LOGIC_FUNCTION_TYPE` 环境变量设置为 `LOCAL` 或 `LAMBDA` 以启用它们。 这可以通过环境变量或管理面板的数据库变量进行配置。 详情请参见[逻辑函数设置指南](/l/zh/developers/self-host/capabilities/setup#logic-functions-available-drivers)。
|
||||
|
||||
### 一键使用 Docker Compose
|
||||
|
||||
#### 无法登录
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
---
|
||||
title: 入站邮件自动回复
|
||||
description: 构建一个使用 AI 对入站邮件进行分拣,并自动以线程形式发送回复的工作流。
|
||||
---
|
||||
|
||||
在数秒内回复入站邮件—而不是数小时。 此工作流使用 AI 智能体筛除噪音(新闻通讯、垃圾邮件、自动回复),为真实邮件起草个性化回复,并在原始会话中以线程形式发送该回复。
|
||||
|
||||
## 电子邮件线程如何运作
|
||||
|
||||
每封电子邮件都包含一个隐藏的 `Message-ID` 头部—由发件人的邮件服务器分配的唯一指纹。 当您回复一封电子邮件时,您的邮件客户端会设置一个 `In-Reply-To` 标头以引用该指纹。 这就是 Gmail、Outlook 以及其他所有客户端将邮件分组为会话的方式。
|
||||
|
||||
在 Twenty 中,该指纹作为 `headerMessageId` 存储在 Message 对象中。 您的工作流会获取它,并将其传递到 Send Email 操作的 In-Reply-To 字段。
|
||||
|
||||
## 构建工作流
|
||||
|
||||
### 步骤 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:查找发件人
|
||||
|
||||
添加 **搜索记录** 操作.
|
||||
|
||||
发件人的地址不在消息本身上——而是在关联的 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 分流并起草回复
|
||||
|
||||
添加一个 **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:根据 AI 的决策进行分支
|
||||
|
||||
添加一个 **If/Else** 操作,用于检查 AI 决定是回复还是跳过。
|
||||
|
||||
| 字段 | 值 |
|
||||
| ------- | ---------------------------------- |
|
||||
| **条件** | AI Agent `response` **不包含** `SKIP` |
|
||||
| **若为真** | 继续执行 Send Email |
|
||||
| **否则** | 不执行任何操作(工作流结束) |
|
||||
|
||||
垃圾邮件、新闻简报以及自动生成的邮件会被丢弃。 其余内容将进入下一步。
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/should-reply.png" style={{width:'100%'}} />
|
||||
|
||||
### 步骤 6:发送会话内回复
|
||||
|
||||
在 "若为真" 分支上添加一个 **Send Email** 操作。 单击 **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 使用主题行将邮件归入同一会话。 要让 Gmail 在原始会话中显示回复,主题行**必须**以 `Re:` 开头(包括冒号和空格)。 否则,即使正确设置了 In-Reply-To 标头,回复也会显示为一个单独的会话。
|
||||
</Warning>
|
||||
|
||||
### 步骤 7:测试并启用
|
||||
|
||||
点击 **Test**,然后检查您的邮件客户端。 回复应当嵌套显示在原始邮件下方。
|
||||
|
||||
确认无误后启用。
|
||||
|
||||
## 进阶构建思路
|
||||
|
||||
* **仅回复 VIP** — 添加一个分支,用于检查发件人的域名,或他们是否在 Twenty 中作为联系人存在
|
||||
* **按意图路由** — 使用不同的 AI Agent 提示词,将销售咨询与支持请求区分处理
|
||||
* **回复前先丰富信息** — 添加一个 Search Records 步骤,将发件人的公司或交易历史拉入 AI 提示词中,以获得更个性化的回复
|
||||
@@ -157,8 +157,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"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -156,8 +156,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"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
---
|
||||
title: Auto-Reply to Inbound Emails
|
||||
description: Build a workflow that uses AI to triage incoming emails and send threaded replies automatically.
|
||||
---
|
||||
|
||||
Respond to inbound emails in seconds — not hours. This workflow uses an AI Agent to filter out noise (newsletters, spam, auto-replies) and draft a personalized response to real messages, then sends it as a threaded reply inside the original conversation.
|
||||
|
||||
## How email threading works
|
||||
|
||||
Every email carries a hidden `Message-ID` header — a unique fingerprint assigned by the sender's mail server. When you reply to an email, your mail client sets an `In-Reply-To` header referencing that fingerprint. That's how Gmail, Outlook, and every other client groups messages into threads.
|
||||
|
||||
In Twenty, that fingerprint is stored as `headerMessageId` on the Message object. Your workflow grabs it and passes it into the Send Email action's In-Reply-To field.
|
||||
|
||||
## Building the workflow
|
||||
|
||||
### Step 1: Create a new workflow
|
||||
|
||||
Head to **Settings -> Workflows** and click **+ New Workflow**.
|
||||
|
||||
### Step 2: Trigger on incoming messages
|
||||
|
||||
Pick **When a Record is Created** and choose **Messages**.
|
||||
|
||||
Every time an email lands in Twenty, this fires.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/workflow-overview.png" style={{width:'100%'}}/>
|
||||
|
||||
### Step 3: Look up who sent it
|
||||
|
||||
Add a **Search Records** action.
|
||||
|
||||
The sender's address isn't on the message itself — it's on the related Message Participant record.
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Object** | Message Participants |
|
||||
| **Filter** | Message **is** `{{trigger.id}}` |
|
||||
| **Filter** | Role **is** From |
|
||||
| **Limit** | 1 |
|
||||
|
||||
This gives you the sender's email in `handle` and their name in `displayName`.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/find-sender.png" style={{width:'100%'}}/>
|
||||
|
||||
### Step 4: AI triage and draft reply
|
||||
|
||||
Add an **AI Agent** action. This single step does two things: decides whether the email deserves a reply, and if so, writes one.
|
||||
|
||||
Use a prompt like:
|
||||
|
||||
```
|
||||
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.
|
||||
```
|
||||
|
||||
The AI Agent outputs its response in a `response` field that the next steps can reference.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/ai-triage.png" style={{width:'100%'}}/>
|
||||
|
||||
### Step 5: Branch on the AI decision
|
||||
|
||||
Add an **If/Else** action to check whether the AI decided to reply or skip.
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Condition** | AI Agent `response` **does not contain** `SKIP` |
|
||||
| **If true** | Continue to Send Email |
|
||||
| **Else** | Do nothing (workflow ends) |
|
||||
|
||||
Spam, newsletters, and auto-generated messages get dropped. Everything else moves to the next step.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/should-reply.png" style={{width:'100%'}}/>
|
||||
|
||||
### Step 6: Send a threaded reply
|
||||
|
||||
Add a **Send Email** action on the "if true" branch. Click **Advanced options**, then **Add In-Reply-To**.
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **To** | `{{Find Sender.first.handle}}` |
|
||||
| **Subject** | `Re: {{trigger.subject}}` |
|
||||
| **Body** | `{{AI Triage & Draft Reply.response}}` |
|
||||
| **In-Reply-To** | `{{trigger.headerMessageId}}` |
|
||||
|
||||
The In-Reply-To field is what makes this a reply instead of a new conversation. The recipient sees it threaded under the original email in Gmail, Outlook, or any other client.
|
||||
|
||||
<img src="/images/user-guide/workflows/auto-reply/send-email.png" style={{width:'100%'}}/>
|
||||
|
||||
<Tip>
|
||||
**In-Reply-To** expects a `message.headerMessageId` from the trigger — it's the email's unique fingerprint, not a recipient address. If you leave it empty, the email still sends, just as a standalone message.
|
||||
</Tip>
|
||||
|
||||
<Warning>
|
||||
Gmail uses the subject line to group messages into threads. The subject **must** start with `Re:` (including the colon and space) for Gmail to display the reply inside the original thread. Without it, the reply will appear as a separate conversation — even if the In-Reply-To header is set correctly.
|
||||
</Warning>
|
||||
|
||||
### Step 7: Test and activate
|
||||
|
||||
Hit **Test**, then check your email client. The reply should appear nested under the original message.
|
||||
|
||||
Activate when you're happy with it.
|
||||
|
||||
## Ideas to build on
|
||||
|
||||
- **Only reply to VIPs** — add a branch that checks the sender's domain or whether they exist as a Contact in Twenty
|
||||
- **Route by intent** — use separate AI Agent prompts to handle sales inquiries differently from support requests
|
||||
- **Enrich before replying** — add a Search Records step to pull the sender's company or deal history into the AI prompt for more personalized replies
|
||||
@@ -3,7 +3,7 @@ import * as projectAnnotations from './preview';
|
||||
|
||||
// Pre-warm the dynamic import used by WorkflowStepDecorator so the
|
||||
// module is cached before any test runs (avoids flaky timeouts in CI).
|
||||
import('~/testing/utils/getTestEnrichedObjectMetadataItemsMock');
|
||||
import('~/testing/utils/generatedMockObjectMetadataItems');
|
||||
|
||||
// This is an important step to apply the right configuration when testing your stories.
|
||||
// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* oxlint-disable no-console, lingui/no-unlocalized-strings */
|
||||
import { print } from 'graphql';
|
||||
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { generateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/utils/generateDepthRecordGqlFieldsFromObject';
|
||||
import { generateFindManyRecordsQuery } from '@/object-record/utils/generateFindManyRecordsQuery';
|
||||
|
||||
@@ -29,7 +29,7 @@ const addTypenamesToSelections = (query: string): string =>
|
||||
|
||||
const toObjectMetadataItems = (rawMetadata: {
|
||||
objects: { edges: { node: Record<string, unknown> }[] };
|
||||
}): EnrichedObjectMetadataItem[] =>
|
||||
}): ObjectMetadataItem[] =>
|
||||
rawMetadata.objects.edges.map((edge) => {
|
||||
const { fieldsList, indexMetadataList, ...rest } = edge.node;
|
||||
|
||||
@@ -44,12 +44,12 @@ const toObjectMetadataItems = (rawMetadata: {
|
||||
indexFieldMetadatas: index.indexFieldMetadataList ?? [],
|
||||
}),
|
||||
),
|
||||
} as unknown as EnrichedObjectMetadataItem;
|
||||
} as unknown as ObjectMetadataItem;
|
||||
});
|
||||
|
||||
const generateForObject = async (
|
||||
token: string,
|
||||
objectMetadataItems: EnrichedObjectMetadataItem[],
|
||||
objectMetadataItems: ObjectMetadataItem[],
|
||||
objectNameSingular: string,
|
||||
) => {
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
|
||||