الانتقال إلى المحتوى الرئيسي
التطبيقات حاليًا في مرحلة الألفا. الميزة تعمل لكنها لا تزال قيد التطور.
توفر حزمة twenty-sdk لبنات بناء مضبوطة الأنواع لإنشاء تطبيقك. تغطي هذه الصفحة كل نوع كيان وكل عميل واجهة برمجة تطبيقات متاح في SDK.

دوال DefineEntity

يوفّر SDK دوالًا لتعريف كيانات تطبيقك. يجب عليك استخدام export default defineEntity({...}) لكي يكتشف SDK الكيانات الخاصة بك. تتحقق هذه الدوال من تكوينك وقت البناء وتوفّر إكمالًا تلقائيًا في بيئة التطوير وأمان الأنواع.
تنظيم الملفات يعود إليك. يعتمد اكتشاف الكيانات على AST — حيث يعثر SDK على استدعاءات export default defineEntity(...) بغض النظر عن مكان وجود الملف. تجميع الملفات حسب النوع (مثلًا، logic-functions/ وroles/) هو مجرّد عرف، وليس متطلبًا.
تُغلّف الأدوار الصلاحيات على كائنات وإجراءات مساحة العمل لديك.
restricted-company-role.ts
import {
  defineRole,
  PermissionFlag,
  STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';

export default defineRole({
  universalIdentifier: '2c80f640-2083-4803-bb49-003e38279de6',
  label: 'My new role',
  description: 'A role that can be used in your workspace',
  canReadAllObjectRecords: false,
  canUpdateAllObjectRecords: false,
  canSoftDeleteAllObjectRecords: false,
  canDestroyAllObjectRecords: false,
  canUpdateAllSettings: false,
  canBeAssignedToAgents: false,
  canBeAssignedToUsers: false,
  canBeAssignedToApiKeys: false,
  objectPermissions: [
    {
      objectUniversalIdentifier:
        STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
      canReadObjectRecords: true,
      canUpdateObjectRecords: true,
      canSoftDeleteObjectRecords: false,
      canDestroyObjectRecords: false,
    },
  ],
  fieldPermissions: [
    {
      objectUniversalIdentifier:
        STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
      fieldUniversalIdentifier:
        STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier,
      canReadFieldValue: false,
      canUpdateFieldValue: false,
    },
  ],
  permissionFlags: [PermissionFlag.APPLICATIONS],
});
يجب أن يحتوي كل تطبيق على استدعاء واحد فقط لـ defineApplication يصف:
  • الهوية: المعرّفات، اسم العرض، والوصف.
  • الأذونات: أيُّ دورٍ تستخدمه وظائفه ومكوّناته الأمامية.
  • (اختياري) المتغيرات: أزواج مفتاح-قيمة تُعرض لوظائفك كمتغيرات بيئة.
  • (اختياري) دوال ما قبل التثبيت/ما بعد التثبيت: دوال منطقية تعمل قبل التثبيت أو بعده.
src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';

export default defineApplication({
  universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
  displayName: 'My Twenty App',
  description: 'My first Twenty app',
  icon: 'IconWorld',
  applicationVariables: {
    DEFAULT_RECIPIENT_NAME: {
      universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
      description: 'Default recipient name for postcards',
      value: 'Jane Doe',
      isSecret: false,
    },
  },
  defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});
الملاحظات:
  • حقول universalIdentifier هي معرّفات حتمية تملكها أنت. أنشِئها مرة واحدة واحتفظ بها ثابتة عبر عمليات المزامنة.
  • applicationVariables تصبح متغيرات بيئة لوظائفك ومكوّناتك الأمامية (على سبيل المثال، DEFAULT_RECIPIENT_NAME متاح كـ process.env.DEFAULT_RECIPIENT_NAME).
  • defaultRoleUniversalIdentifier يجب أن يُشير إلى دور مُعرَّف باستخدام defineRole() (انظر أعلاه).
  • يتم اكتشاف دوال ما قبل التثبيت وما بعده تلقائيًا أثناء بناء البيان — لا حاجة للإشارة إليها في defineApplication().

بيانات التعريف لسوق التطبيقات

إذا كنت تخطط لـ نشر تطبيقك، فإن هذه الحقول الاختيارية تتحكّم في كيفية ظهوره في السوق:
الحقلالوصف
authorاسم المؤلف أو الشركة
categoryفئة التطبيق لتصفية سوق التطبيقات
logoUrlمسار شعار تطبيقك (مثلًا، public/logo.png)
screenshotsمصفوفة لمسارات لقطات الشاشة (مثلًا، public/screenshot-1.png)
aboutDescriptionوصف ماركداون أطول لعلامة التبويب “حول”. إذا لم يتم تضمينه، يستخدم السوق ملف README.md الخاص بالحزمة من npm
websiteUrlرابط إلى موقعك الإلكتروني
termsUrlرابط إلى شروط الخدمة
emailSupportعنوان البريد الإلكتروني للدعم
issueReportUrlرابط إلى متتبّع المشاكل

الأدوار والصلاحيات

يُحدّد الحقل defaultRoleUniversalIdentifier في application-config.ts الدور الافتراضي الذي تستخدمه وظائف المنطق والمكوّنات الأمامية في تطبيقك. راجع defineRole أعلاه للحصول على التفاصيل.
  • رمز وقت التشغيل المحقون باسم TWENTY_APP_ACCESS_TOKEN مستمد من هذا الدور.
  • العميل مضبوط الأنواع مقيَّد بالأذونات الممنوحة لذلك الدور.
  • اتبع مبدأ أقل الامتياز: أنشئ دورًا مخصصًا يضم فقط الأذونات التي تحتاجها وظائفك.
الدور الافتراضي للوظيفة
عند توليد تطبيق جديد بالقالب، ينشئ CLI ملفّ دور افتراضي:
src/roles/default-role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk';

export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
  'b648f87b-1d26-4961-b974-0908fd991061';

export default defineRole({
  universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
  label: 'Default function role',
  description: 'Default role for function Twenty client',
  canReadAllObjectRecords: true,
  canUpdateAllObjectRecords: false,
  canSoftDeleteAllObjectRecords: false,
  canDestroyAllObjectRecords: false,
  canUpdateAllSettings: false,
  canBeAssignedToAgents: false,
  canBeAssignedToUsers: false,
  canBeAssignedToApiKeys: false,
  objectPermissions: [],
  fieldPermissions: [],
  permissionFlags: [],
});
يُشار إلى universalIdentifier لهذا الدور في application-config.ts باسم defaultRoleUniversalIdentifier:
  • *.role.ts يحدد ما يمكن أن يفعله الدور.
  • application-config.ts يشير إلى ذلك الدور بحيث ترث وظائفك أذوناته.
الملاحظات:
  • ابدأ من الدور المُنشأ بالقالب، ثم قيّده تدريجيًا باتباع مبدأ أقل الامتياز.
  • استبدل objectPermissions وfieldPermissions بالكائنات والحقول التي تحتاجها وظائفك فعليًا.
  • permissionFlags تتحكم في الوصول إلى القدرات على مستوى المنصة. اجعلها في حدّها الأدنى.
  • اطّلع على مثال عملي: hello-world/src/roles/function-role.ts.
تصف الكائنات المخصصة كلًا من المخطط والسلوك للسجلات في مساحة عملك. استخدم defineObject() لتعريف كائنات مع تحقق مدمج:
postCard.object.ts
import { defineObject, FieldType } from 'twenty-sdk';

enum PostCardStatus {
  DRAFT = 'DRAFT',
  SENT = 'SENT',
  DELIVERED = 'DELIVERED',
  RETURNED = 'RETURNED',
}

export default defineObject({
  universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
  nameSingular: 'postCard',
  namePlural: 'postCards',
  labelSingular: 'Post Card',
  labelPlural: 'Post Cards',
  description: 'A post card object',
  icon: 'IconMail',
  fields: [
    {
      universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
      name: 'content',
      type: FieldType.TEXT,
      label: 'Content',
      description: "Postcard's content",
      icon: 'IconAbc',
    },
    {
      universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
      name: 'recipientName',
      type: FieldType.FULL_NAME,
      label: 'Recipient name',
      icon: 'IconUser',
    },
    {
      universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
      name: 'recipientAddress',
      type: FieldType.ADDRESS,
      label: 'Recipient address',
      icon: 'IconHome',
    },
    {
      universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
      name: 'status',
      type: FieldType.SELECT,
      label: 'Status',
      icon: 'IconSend',
      defaultValue: `'${PostCardStatus.DRAFT}'`,
      options: [
        { value: PostCardStatus.DRAFT, label: 'Draft', position: 0, color: 'gray' },
        { value: PostCardStatus.SENT, label: 'Sent', position: 1, color: 'orange' },
        { value: PostCardStatus.DELIVERED, label: 'Delivered', position: 2, color: 'green' },
        { value: PostCardStatus.RETURNED, label: 'Returned', position: 3, color: 'orange' },
      ],
    },
    {
      universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
      name: 'deliveredAt',
      type: FieldType.DATE_TIME,
      label: 'Delivered at',
      icon: 'IconCheck',
      isNullable: true,
      defaultValue: null,
    },
  ],
});
النقاط الرئيسية:
  • استخدم defineObject() للحصول على تحقق مدمج ودعم أفضل من IDE.
  • universalIdentifier يجب أن يكون فريدًا وثابتًا عبر عمليات النشر.
  • يتطلب كل حقل name وtype وlabel ومعرّف universalIdentifier ثابتًا خاصًا به.
  • المصفوفة fields اختيارية — يمكنك تعريف كائنات بدون حقول مخصصة.
  • يمكنك إنشاء كائنات جديدة باستخدام yarn twenty add، والذي يرشدك خلال التسمية والحقول والعلاقات.
يتم إنشاء الحقول الأساسية تلقائيًا. عند تعريف كائن مخصص، يضيف Twenty تلقائيًا حقولًا قياسية مثل id وname وcreatedAt وupdatedAt وcreatedBy وupdatedBy وdeletedAt. لا تحتاج إلى تعريف هذه في مصفوفة fields — أضف فقط حقولك المخصصة. يمكنك تجاوز الحقول الافتراضية من خلال تعريف حقل بالاسم نفسه في مصفوفة fields الخاصة بك، لكن هذا غير مستحسن.
استخدم defineField() لإضافة حقول إلى كائنات لا تملكها — مثل كائنات Twenty القياسية (Person, Company, etc.) أو كائنات من تطبيقات أخرى. على خلاف الحقول المضمّنة في defineObject()، تتطلّب الحقول المستقلة objectUniversalIdentifier لتحديد الكائن الذي تقوم بتوسيعه:
src/fields/company-loyalty-tier.field.ts
import { defineField, FieldType } from 'twenty-sdk';

export default defineField({
  universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890',
  objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object
  name: 'loyaltyTier',
  type: FieldType.SELECT,
  label: 'Loyalty Tier',
  icon: 'IconStar',
  options: [
    { value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' },
    { value: 'SILVER', label: 'Silver', position: 1, color: 'gray' },
    { value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' },
  ],
});
النقاط الرئيسية:
  • objectUniversalIdentifier يحدّد الكائن الهدف. بالنسبة للكائنات القياسية، استخدم STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS المُصدَّر من twenty-sdk.
  • عند تعريف الحقول بشكل مضمّن في defineObject()، لا تحتاج إلى objectUniversalIdentifier — إذ يُورَّث من الكائن الأب.
  • defineField() هي الطريقة الوحيدة لإضافة حقول إلى كائنات لم تُنشئها باستخدام defineObject().
تربط العلاقات الكائنات معًا. في Twenty، تكون العلاقات دائمًا ثنائية الاتجاه — حيث تعرّف الجانبين، ويشير كل جانب إلى الآخر.هناك نوعان من العلاقات:
نوع العلاقةالوصفهل لديه مفتاح خارجي؟
MANY_TO_ONEتشير العديد من سجلات هذا الكائن إلى سجل واحد من الهدفنعم (joinColumnName)
ONE_TO_MANYيحتوي سجل واحد من هذا الكائن على العديد من سجلات الهدفلا (الجانب العكسي)

كيف تعمل العلاقات

تتطلّب كل علاقة حقلين يشيران إلى بعضهما البعض:
  1. جانب MANY_TO_ONE — يوجد على الكائن الذي يحمل المفتاح الخارجي
  2. جانب ONE_TO_MANY — يوجد على الكائن الذي يملك المجموعة
يستخدم كلا الحقلين FieldType.RELATION ويُحيل كلٌ منهما إلى الآخر عبر relationTargetFieldMetadataUniversalIdentifier.

مثال: البطاقة البريدية لديها العديد من المستلمين

افترض أن PostCard يمكن إرسالها إلى العديد من سجلات PostCardRecipient. ينتمي كل مستلم إلى بطاقة بريدية واحدة بالضبط.الخطوة 1: عرّف جانب ONE_TO_MANY على PostCard (جانب “الواحد”):
src/fields/post-card-recipients-on-post-card.field.ts
import { defineField, FieldType, RelationType } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';

// Export so the other side can reference it
export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111';
// Import from the other side
import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field';

export default defineField({
  universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
  objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
  type: FieldType.RELATION,
  name: 'postCardRecipients',
  label: 'Post Card Recipients',
  icon: 'IconUsers',
  relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
  relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
  universalSettings: {
    relationType: RelationType.ONE_TO_MANY,
  },
});
الخطوة 2: عرّف جانب MANY_TO_ONE على PostCardRecipient (جانب “العديد” — يحمل المفتاح الخارجي):
src/fields/post-card-on-post-card-recipient.field.ts
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';

// Export so the other side can reference it
export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222';
// Import from the other side
import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field';

export default defineField({
  universalIdentifier: POST_CARD_FIELD_ID,
  objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
  type: FieldType.RELATION,
  name: 'postCard',
  label: 'Post Card',
  icon: 'IconMail',
  relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
  relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
  universalSettings: {
    relationType: RelationType.MANY_TO_ONE,
    onDelete: OnDeleteAction.CASCADE,
    joinColumnName: 'postCardId',
  },
});
الاستيرادات الدائرية: كلا حقلي العلاقة يُحيل كلٌ منهما إلى universalIdentifier الخاص بالآخر. لتجنّب مشكلات الاستيراد الدائري، صدّر معرّفات الحقول كثوابت مسمّاة من كل ملف، واستوردها في الملف الآخر. يقوم نظام البناء بحلّها في وقت الترجمة.

الربط مع الكائنات القياسية

لإنشاء علاقة مع كائن Twenty مضمّن (Person, Company, etc.)، استخدم STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS:
src/fields/person-on-self-hosting-user.field.ts
import {
  defineField,
  FieldType,
  RelationType,
  OnDeleteAction,
  STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object';

export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333';
export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444';

export default defineField({
  universalIdentifier: PERSON_FIELD_ID,
  objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER,
  type: FieldType.RELATION,
  name: 'person',
  label: 'Person',
  description: 'Person matching with the self hosting user',
  isNullable: true,
  relationTargetObjectMetadataUniversalIdentifier:
    STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
  relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID,
  universalSettings: {
    relationType: RelationType.MANY_TO_ONE,
    onDelete: OnDeleteAction.SET_NULL,
    joinColumnName: 'personId',
  },
});

خصائص حقل العلاقة

الخاصيةمطلوبالوصف
typeنعميجب أن يكون FieldType.RELATION
relationTargetObjectMetadataUniversalIdentifierنعمقيمة universalIdentifier للكائن الهدف
relationTargetFieldMetadataUniversalIdentifierنعمقيمة universalIdentifier للحقل المطابق على الكائن الهدف
universalSettings.relationTypeنعمRelationType.MANY_TO_ONE أو RelationType.ONE_TO_MANY
universalSettings.onDeleteMANY_TO_ONE فقطماذا يحدث عند حذف السجل المشار إليه: CASCADE، SET_NULL، RESTRICT، أو NO_ACTION
universalSettings.joinColumnNameMANY_TO_ONE فقطاسم عمود قاعدة البيانات للمفتاح الخارجي (مثل postCardId)

حقول العلاقات المضمّنة في defineObject

يمكنك أيضًا تعريف حقول العلاقات مباشرةً داخل defineObject(). في هذه الحالة، احذف objectUniversalIdentifier — إذ يُورَّث من الكائن الأب:
export default defineObject({
  universalIdentifier: '...',
  nameSingular: 'postCardRecipient',
  // ...
  fields: [
    {
      universalIdentifier: POST_CARD_FIELD_ID,
      type: FieldType.RELATION,
      name: 'postCard',
      label: 'Post Card',
      relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
      relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
      universalSettings: {
        relationType: RelationType.MANY_TO_ONE,
        onDelete: OnDeleteAction.CASCADE,
        joinColumnName: 'postCardId',
      },
    },
    // ... other fields
  ],
});
كل ملف وظيفة يستخدم defineLogicFunction() لتصدير تكوين مع معالج ومشغّلات اختيارية.
src/logic-functions/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import { CoreApiClient, type Person } from 'twenty-client-sdk/core';

const handler = async (params: RoutePayload) => {
  const client = new CoreApiClient();
  const name = 'name' in params.queryStringParameters
    ? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
    : 'Hello world';

  const result = await client.mutation({
    createPostCard: {
      __args: { data: { name } },
      id: true,
      name: true,
    },
  });
  return result;
};

export default defineLogicFunction({
  universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
  name: 'create-new-post-card',
  timeoutSeconds: 2,
  handler,
  httpRouteTriggerSettings: {
    path: '/post-card/create',
    httpMethod: 'GET',
    isAuthRequired: true,
  },
  /*databaseEventTriggerSettings: {
    eventName: 'people.created',
  },*/
  /*cronTriggerSettings: {
    pattern: '0 0 1 1 *',
  },*/
});
أنواع المشغّلات المتاحة:
  • httpRoute: يعرِض وظيفتك على مسار وطريقة HTTP تحت نقطة النهاية /s/:
مثال: path: '/post-card/create' يمكن استدعاؤه عبر https://your-twenty-server.com/s/post-card/create
  • cron: يشغّل وظيفتك على جدول باستخدام تعبير CRON.
  • databaseEvent: يعمل على أحداث دورة حياة كائنات مساحة العمل. عندما تكون عملية الحدث هي updated، يمكن تحديد الحقول المحددة المراد الاستماع إليها في مصفوفة updatedFields. إذا تُركت غير معرّفة أو فارغة، فسيؤدي أي تحديث إلى تشغيل الدالة.
مثال: person.updated، *.created، company.*
يمكنك أيضًا تنفيذ دالة يدويًا باستخدام CLI:
yarn twenty exec -n create-new-post-card -p '{"key": "value"}'
yarn twenty exec -y e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
يمكنك متابعة السجلات باستخدام:
yarn twenty logs

حمولة مشغل المسار

عندما يستدعي مُشغِّل المسار وظيفتك المنطقية، فإنها تتلقّى كائن RoutePayload الذي يتبع صيغة AWS HTTP API v2. استورد نوع RoutePayload من twenty-sdk:
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';

const handler = async (event: RoutePayload) => {
  const { headers, queryStringParameters, pathParameters, body } = event;
  const { method, path } = event.requestContext.http;

  return { message: 'Success' };
};
يحتوي نوع RoutePayload على البنية التالية:
الخاصيةالنوعالوصفمثال
headersRecord<string, string | undefined>رؤوس HTTP (فقط تلك المدرجة في forwardedRequestHeaders)انظر القسم أدناه
queryStringParametersRecord<string, string | undefined>معلمات سلسلة الاستعلام (تُضمّ القيم المتعددة باستخدام فواصل)/users?ids=1&ids=2&ids=3&name=Alice -> { ids: '1,2,3', name: 'Alice' }
pathParametersRecord<string, string | undefined>معلمات المسار المستخرجة من نمط المسار/users/:id, /users/123 -> { id: '123' }
المحتوىobject | nullجسم الطلب المُحلَّل (JSON){ id: 1 } -> { id: 1 }
isBase64Encodedقيمة منطقيةما إذا كان جسم الطلب مُرمَّزًا بترميز base64
requestContext.http.methodstringطريقة HTTP (GET, POST, PUT, PATCH, DELETE)
requestContext.http.pathstringالمسار الخام للطلب

forwardedRequestHeaders

افتراضيًا، لا تُمرَّر رؤوس HTTP من الطلبات الواردة إلى دالتك المنطقية لأسباب أمنية. للوصول إلى رؤوس محددة، أدرِجها في مصفوفة forwardedRequestHeaders:
export default defineLogicFunction({
  universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
  name: 'webhook-handler',
  handler,
  httpRouteTriggerSettings: {
    path: '/webhook',
    httpMethod: 'POST',
    isAuthRequired: false,
    forwardedRequestHeaders: ['x-webhook-signature', 'content-type'],
  },
});
في معالجك، يمكنك الوصول إلى الرؤوس المُمرَّرة بهذه الطريقة:
const handler = async (event: RoutePayload) => {
  const signature = event.headers['x-webhook-signature'];
  const contentType = event.headers['content-type'];

  // Validate webhook signature...
  return { received: true };
};
تُحوَّل أسماء الرؤوس إلى أحرف صغيرة. يمكنك الوصول إليها باستخدام مفاتيح بأحرف صغيرة (على سبيل المثال، event.headers['content-type']).

إتاحة دالة كأداة

يمكن إتاحة الدوال المنطقية بوصفها أدوات لوكلاء الذكاء الاصطناعي وسير العمل. عند تمييز دالة كأداة، تصبح قابلة للاكتشاف بواسطة ميزات الذكاء الاصطناعي في Twenty ويمكن استخدامها في أتمتة سير العمل.لتمييز دالة منطقية كأداة، عيِّن isTool: true:
src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-client-sdk/core';

const handler = async (params: { companyName: string; domain?: string }) => {
  const client = new CoreApiClient();

  const result = await client.mutation({
    createTask: {
      __args: {
        data: {
          title: `Enrich data for ${params.companyName}`,
          body: `Domain: ${params.domain ?? 'unknown'}`,
        },
      },
      id: true,
    },
  });

  return { taskId: result.createTask.id };
};

export default defineLogicFunction({
  universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
  name: 'enrich-company',
  description: 'Enrich a company record with external data',
  timeoutSeconds: 10,
  handler,
  isTool: true,
});
النقاط الرئيسية:
  • يمكنك دمج isTool مع المشغِّلات — إذ يمكن للدالة أن تكون أداة (قابلة للاستدعاء من قِبل وكلاء الذكاء الاصطناعي) وأن تُشغَّل بواسطة الأحداث في الوقت نفسه.
  • toolInputSchema (اختياري): كائن JSON Schema يصف المعلمات التي تقبلها دالتك. يُحسَب المخطط تلقائيًا من خلال تحليل ساكن للشيفرة المصدرية، ولكن يمكنك تعيينه صراحةً:
export default defineLogicFunction({
  ...,
  toolInputSchema: {
    type: 'object',
    properties: {
      companyName: {
        type: 'string',
        description: 'The name of the company to enrich',
      },
      domain: {
        type: 'string',
        description: 'The company website domain (optional)',
      },
    },
    required: ['companyName'],
  },
});
اكتب description جيدًا. يعتمد وكلاء الذكاء الاصطناعي على حقل description الخاص بالدالة لتحديد وقت استخدام الأداة. كن محددًا بشأن ما تفعله الأداة ومتى ينبغي استدعاؤها.
دالة ما بعد التثبيت هي دالة منطقية تعمل تلقائيًا بعد تثبيت تطبيقك على مساحة عمل. ينفّذه الخادم بعد مزامنة البيانات الوصفية للتطبيق وإنشاء عميل SDK، بحيث تكون مساحة العمل جاهزة تمامًا للاستخدام ويكون المخطط الجديد مطبَّقًا. تشمل حالات الاستخدام النموذجية تهيئة البيانات الافتراضية، وإنشاء السجلات الأولية، وتكوين إعدادات مساحة العمل، أو توفير الموارد على خدمات جهات خارجية.
src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk';

const handler = async (payload: InstallPayload): Promise<void> => {
  console.log('Post install logic function executed successfully!', payload.previousVersion);
};

export default definePostInstallLogicFunction({
  universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
  name: 'post-install',
  description: 'Runs after installation to set up the application.',
  timeoutSeconds: 300,
  shouldRunOnVersionUpgrade: false,
  shouldRunSynchronously: false,
  handler,
});
يمكنك أيضًا تنفيذ دالة ما بعد التثبيت يدويًا في أي وقت باستخدام CLI:
yarn twenty exec --postInstall
النقاط الرئيسية:
  • تستخدم دوال ما بعد التثبيت definePostInstallLogicFunction() — وهو إصدار متخصص يستبعد إعدادات المُشغِّل (cronTriggerSettings وdatabaseEventTriggerSettings وhttpRouteTriggerSettings وisTool).
  • يتلقى المعالج InstallPayload يحتوي على { previousVersion?: string; newVersion: string } — حيث إن newVersion هو الإصدار الجاري تثبيته، وpreviousVersion هو الإصدار الذي كان مُثبّتًا سابقًا (أو undefined عند التثبيت الأولي). استخدم هذه القيم للتمييز بين عمليات التثبيت الجديدة والترقيات ولتشغيل منطق الترحيل الخاص بالإصدار.
  • موعد تشغيل الخطاف: في عمليات التثبيت الجديدة فقط، افتراضيًا. مرّر shouldRunOnVersionUpgrade: true إذا كنت تريد تشغيله أيضًا عند ترقية التطبيق من إصدار سابق. عند إغفاله، تكون القيمة الافتراضية للعلم false، وتتجاوز الترقيات هذا الخطاف.
  • نموذج التنفيذ — غير متزامن افتراضيًا، والتزامني اختياري: يتحكّم العلم shouldRunSynchronously في كيفية تنفيذ ما بعد التثبيت.
    • shouldRunSynchronously: false (الإعداد الافتراضي) — يتم إدراج الخطاف في قائمة الرسائل مع retryLimit: 3 ويعمل بشكل غير متزامن داخل عامل عمل. يعود ردّ التثبيت بمجرد وضع المهمة في الطابور، لذا فإن معالجًا بطيئًا أو متعطلًا لا يحجب المستدعي. سيُجرِّب العامل إعادة المحاولة حتى ثلاث مرات. استخدم هذا للمهام طويلة التشغيل — بَذر مجموعات بيانات كبيرة، استدعاء واجهات برمجة تطبيقات خارجية بطيئة، تهيئة موارد خارجية، أو أي شيء قد يتجاوز نافذة استجابة HTTP المعقولة.
    • shouldRunSynchronously: true — يُنفّذ الخطاف ضمن تدفّق التثبيت مباشرةً (نفس المنفِّذ كما قبل التثبيت). يَحجُب طلب التثبيت حتى ينتهي المعالج، وإذا رمى استثناءً، سيتلقى مستدعي التثبيت POST_INSTALL_ERROR. لا توجد محاولات إعادة تلقائية. استخدم هذا للمهام السريعة التي يجب إكمالها قبل الاستجابة — مثل إظهار خطأ تحقق للمستخدم، أو إعداد سريع سيعتمد عليه العميل مباشرةً بعد عودة نداء التثبيت. ضع في اعتبارك أن ترحيل البيانات الوصفية يكون قد طُبِّق بالفعل عند تشغيل ما بعد التثبيت، لذلك فإن فشل الوضع المتزامن لا يعيد التغييرات على المخطط إلى الوراء — بل يكتفي بإبراز الخطأ.
  • تأكّد من أن معالجك قابل للتنفيذ المتكرر دون آثار جانبية. في الوضع غير المتزامن قد تُعيد قائمة الانتظار المحاولة حتى ثلاث مرات؛ وفي أي من الوضعين قد يعمل الخطاف مجددًا أثناء الترقيات عند ضبط shouldRunOnVersionUpgrade: true.
  • متغيرات البيئة APPLICATION_ID وAPP_ACCESS_TOKEN وAPI_URL متاحة داخل المعالج (كما في أي دالة منطق أخرى)، لذا يمكنك استدعاء واجهة Twenty API باستخدام رمز وصول للتطبيق مقيّد بنطاق تطبيقك.
  • يُسمح بدالة ما بعد التثبيت واحدة فقط لكل تطبيق. سيُنتج إنشاء ملف البيان خطأً إذا تم اكتشاف أكثر من واحدة.
  • تُرفَق خصائص الدالة universalIdentifier وshouldRunOnVersionUpgrade وshouldRunSynchronously تلقائيًا ببيان التطبيق ضمن الحقل postInstallLogicFunction أثناء عملية البناء — ولا تحتاج إلى الإشارة إليها في defineApplication().
  • تم تعيين مهلة افتراضية إلى 300 ثانية (5 دقائق) للسماح بمهام الإعداد الأطول مثل تهيئة البيانات.
  • لا يُنفَّذ في وضع التطوير: عند تسجيل تطبيق محليًا (عبر yarn twenty dev)، يتجاوز الخادم تدفّق التثبيت بالكامل ويُزامن الملفات مباشرةً عبر مراقِب CLI — لذا لن يعمل ما بعد التثبيت في وضع التطوير مطلقًا، بغضّ النظر عن shouldRunSynchronously. استخدم yarn twenty exec --postInstall لتشغيله يدويًا على مساحة عمل قيد التشغيل.
دالة ما قبل التثبيت هي دالة منطقية تعمل تلقائيًا أثناء التثبيت، قبل تطبيق ترحيل البيانات الوصفية لمساحة العمل. تتشارك نفس بنية الحمولة مع ما بعد التثبيت (InstallPayload)، لكنها موضوعة أبكر في تدفّق التثبيت كي تجهّز حالة يعتمد عليها الترحيل القادم — ومن الاستخدامات الشائعة: نسخ البيانات احتياطيًا، التحقق من التوافق مع المخطط الجديد، أو أرشفة السجلات التي ستُعاد هيكلتها أو ستُحذف.
src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk';

const handler = async (payload: InstallPayload): Promise<void> => {
  console.log('Pre install logic function executed successfully!', payload.previousVersion);
};

export default definePreInstallLogicFunction({
  universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
  name: 'pre-install',
  description: 'Runs before installation to prepare the application.',
  timeoutSeconds: 300,
  shouldRunOnVersionUpgrade: true,
  handler,
});
يمكنك أيضًا تنفيذ دالة ما قبل التثبيت يدويًا في أي وقت باستخدام CLI:
yarn twenty exec --preInstall
النقاط الرئيسية:
  • تستخدم دوال ما قبل التثبيت definePreInstallLogicFunction() — نفس الإعدادات المتخصصة كما في ما بعد التثبيت، لكنها مرتبطة بموضع مختلف ضمن دورة الحياة.
  • يتلقّى كلٌّ من معالجي ما قبل التثبيت وما بعد التثبيت النوع نفسه InstallPayload: { previousVersion?: string; newVersion: string }. استورده مرة واحدة وأعد استخدامه لكلا الخطافين.
  • موعد تشغيل الخطاف: موضوع مباشرةً قبل ترحيل البيانات الوصفية لمساحة العمل (synchronizeFromManifest). قبل التنفيذ، يُشغِّل الخادم مزامنة “pared-down sync” ذات طابع إضافي فقط تقوم بتسجيل دالة ما قبل التثبيت للإصدار الجديد في البيانات الوصفية لمساحة العمل — دون لمس أي شيء آخر — ثم يُنفّذها. لأن هذه المزامنة «إضافية فقط»، تبقى كائنات وحقول وبيانات الإصدار السابق سليمة عند تشغيل معالجك: يمكنك قراءة حالة ما قبل الترحيل ونسخها احتياطيًا بأمان.
  • نموذج التنفيذ: يُنفَّذ ما قبل التثبيت بشكل متزامن ويحجب عملية التثبيت. إذا رمى المعالج استثناءً، تُلغى عملية التثبيت قبل تطبيق أي تغييرات على المخطط — وتبقى مساحة العمل على الإصدار السابق بحالة متّسقة. هذا مقصود: ما قبل التثبيت هو فرصتك الأخيرة لرفض ترقية تنطوي على مخاطر.
  • كما هو الحال مع ما بعد التثبيت، يُسمح بدالة ما قبل التثبيت واحدة فقط لكل تطبيق. تُربَط تلقائيًا ببيان التطبيق تحت preInstallLogicFunction أثناء عملية البناء.
  • لا يُنفَّذ في وضع التطوير: كما في ما بعد التثبيت — يتم تجاوز تدفّق التثبيت بالكامل للتطبيقات المسجّلة محليًا، لذا لن يعمل ما قبل التثبيت مطلقًا عند yarn twenty dev. استخدم yarn twenty exec --preInstall لتشغيله يدويًا.
كلا الخطافين جزء من تدفّق التثبيت نفسه ويتلقّيان نفس InstallPayload. الاختلاف يكمن في موعد تشغيلهما نسبةً إلى ترحيل البيانات الوصفية لمساحة العمل، وهذا يغيّر البيانات التي يمكنهما التعامل معها بأمان.
┌─────────────────────────────────────────────────────────────┐
│ install flow                                                │
│                                                             │
│   upload package → [pre-install] → metadata migration →     │
│   generate SDK → [post-install]                             │
│                                                             │
│                  old schema visible    new schema visible   │
└─────────────────────────────────────────────────────────────┘
ما قبل التثبيت دائمًا متزامن (يحجب التثبيت ويمكنه إحباطه). ما بعد التثبيت غير متزامن افتراضيًا — يُدرج على عامل مع محاولات إعادة تلقائية — لكن يمكن التبديل إلى تنفيذ متزامن عبر shouldRunSynchronously: true. راجع الأكورديون definePostInstallLogicFunction أعلاه لمعرفة متى تستخدم كل وضع.استخدم post-install لأي شيء يتطلّب وجود المخطط الجديد. وهذا هو السيناريو الشائع:
  • بَذر بيانات افتراضية (إنشاء سجلات أولية وعروض افتراضية ومحتوى تجريبي) للكائنات والحقول المضافة حديثًا.
  • تسجيل خطافات الويب مع خدمات أطراف ثالثة بعد أن حصل التطبيق على بيانات الاعتماد الخاصة به.
  • استدعاء واجهة برمجة التطبيقات الخاصة بك لإكمال إعداد يعتمد على البيانات الوصفية المتزامنة.
  • منطق idempotent لتحقيق “تأكّد من وجود هذا” والذي ينبغي مواءمة الحالة في كل ترقية — بالاقتران مع shouldRunOnVersionUpgrade: true.
مثال — بَذر سجل PostCard افتراضي بعد التثبيت:
src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
import { createClient } from './generated/client';

const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
  if (previousVersion) return; // fresh installs only

  const client = createClient();
  await client.postCard.create({
    data: { title: 'Welcome to Postcard', content: 'Your first card!' },
  });
};

export default definePostInstallLogicFunction({
  universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
  name: 'post-install',
  description: 'Seeds a welcome post card after install.',
  timeoutSeconds: 300,
  shouldRunOnVersionUpgrade: false,
  handler,
});
استخدم pre-install عندما قد يُتلف الترحيل أو يدمّر البيانات الحالية. لأن ما قبل التثبيت يعمل مقابل المخطط السابق وفشله يُرجِع الترقية إلى الوراء، فهو المكان المناسب لأي شيء محفوف بالمخاطر:
  • نسخ البيانات احتياطيًا قبل حذفها أو إعادة هيكلتها — مثل إزالة حقل في v2 وتحتاج إلى نسخ قيمه إلى حقل آخر أو تصديرها إلى التخزين قبل تشغيل الترحيل.
  • أرشفة السجلات التي سيبطلها قيد جديد — مثل أن يصبح حقل ما NOT NULL وتحتاج أولًا إلى حذف الصفوف ذات القيم الفارغة أو إصلاحها.
  • التحقق من التوافق ورفض الترقية إذا تعذّر ترحيل البيانات الحالية بسلاسة — ارمِ من داخل المعالج وسيُلغى التثبيت دون تطبيق أي تغييرات. هذا أكثر أمانًا من اكتشاف عدم التوافق في منتصف الترحيل.
  • إعادة تسمية البيانات أو إعادة تعيين مفاتيحها قبل تغيير في المخطط قد يؤدي إلى فقدان الارتباط.
مثال — أرشف السجلات قبل ترحيل هدّام:
src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
import { createClient } from './generated/client';

const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
  // Only the 1.x → 2.x upgrade drops the legacy `notes` field.
  if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
    return;
  }

  const client = createClient();
  const legacyRecords = await client.postCard.findMany({
    where: { notes: { isNotNull: true } },
  });

  if (legacyRecords.length === 0) return;

  // Copy legacy `notes` into the new `description` field before the migration
  // drops the `notes` column. If this fails, the upgrade is aborted and the
  // workspace stays on v1 with all data intact.
  await Promise.all(
    legacyRecords.map((record) =>
      client.postCard.update({
        where: { id: record.id },
        data: { description: record.notes },
      }),
    ),
  );
};

export default definePreInstallLogicFunction({
  universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
  name: 'pre-install',
  description: 'Backs up legacy notes into description before the v2 migration.',
  timeoutSeconds: 300,
  shouldRunOnVersionUpgrade: true,
  handler,
});
قاعدة عامة:
ترغب في…استخدام
بذر بيانات افتراضية، تهيئة مساحة العمل، تسجيل موارد خارجيةpost-install
تشغيل بذر طويل الأمد أو استدعاءات أطراف ثالثة لا ينبغي أن تحجب استجابة التثبيتpost-install (الإعداد الافتراضي — shouldRunSynchronously: false، مع محاولات إعادة من العامل)
تشغيل إعداد سريع سيعتمد عليه المستدعي مباشرةً بعد عودة نداء التثبيتpost-install مع shouldRunSynchronously: true
قراءة البيانات أو نسخها احتياطيًا والتي قد يفقدها الترحيل القادمpre-install
رفض ترقية قد تُفسد البيانات الحاليةpre-install (ارمِ من المعالج)
تنفيذ مواءمة في كل ترقيةpost-install مع shouldRunOnVersionUpgrade: true
تنفيذ إعداد لمرة واحدة في التثبيت الأول فقطpost-install مع shouldRunOnVersionUpgrade: false (الإعداد الافتراضي)
إذا ساورك الشك، فاجعل الافتراضي هو post-install. الجأ إلى ما قبل التثبيت فقط عندما يكون الترحيل نفسه هدّامًا وتحتاج إلى التقاط الحالة السابقة قبل أن تزول.
المكوّنات الأمامية هي مكوّنات React تُعرَض مباشرة داخل واجهة مستخدم Twenty. تعمل ضمن Web Worker معزول باستخدام Remote DOM — تكون شيفرتك في صندوق عزل لكنها تُعرَض أصيلًا داخل الصفحة، وليس ضمن iframe.

أين يمكن استخدام مكوّنات الواجهة الأمامية

يمكن عرض مكوّنات الواجهة الأمامية في موقعين داخل Twenty:
  • اللوحة الجانبية — المكوّنات غير عديمة الرأس تفتح في اللوحة الجانبية اليمنى. هذا هو السلوك الافتراضي عندما يتم تشغيل مكوّن واجهة أمامية من قائمة الأوامر.
  • الويدجت (لوحات المعلومات وصفحات السجلات) — يمكن تضمين مكوّنات الواجهة الأمامية كويدجت داخل تخطيطات الصفحات. عند تكوين لوحة معلومات أو تخطيط صفحة سجل، يمكن للمستخدمين إضافة ويدجت لمكوّن واجهة أمامية.

مثال أساسي

أسرع طريقة لرؤية مكوّن أمامي قيد العمل هي تسجيله كأمر. إضافة حقل command مع isPinned: true يجعلُه يظهر كزر إجراء سريع في الزاوية العلوية اليمنى من الصفحة — دون الحاجة إلى تخطيط صفحة:
src/front-components/hello-world.tsx
import { defineFrontComponent } from 'twenty-sdk';

const HelloWorld = () => {
  return (
    <div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
      <h1>Hello from my app!</h1>
      <p>This component renders inside Twenty.</p>
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
  name: 'hello-world',
  description: 'A simple front component',
  component: HelloWorld,
  command: {
    universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
    shortLabel: 'Hello',
    label: 'Hello World',
    icon: 'IconBolt',
    isPinned: true,
    availabilityType: 'GLOBAL',
  },
});
بعد المزامنة باستخدام yarn twenty dev (أو تشغيل الأمر لمرة واحدة yarn twenty dev --once)، يظهر الإجراء السريع في الزاوية العلوية اليمنى من الصفحة:
زر إجراء سريع في الزاوية العلوية اليمنى
انقره لعرض المكوّن مضمنًا داخل الصفحة.

حقول التكوين

الحقلمطلوبالوصف
universalIdentifierنعممعرّف فريد ثابت لهذا المكوّن
componentنعمدالة مكوّن React
nameلااسم العرض
الوصفلاوصف لما يفعله المكوّن
isHeadlessلاعيِّنه إلى true إذا كان المكوّن بلا واجهة مرئية (انظر أدناه)
أمرلاسجّل المكوّن كأمر (انظر خيارات الأوامر أدناه)

وضع مكوّن أمامي على صفحة

إضافةً إلى الأوامر، يمكنك تضمين مكوّن أمامي مباشرةً في صفحة سجل عبر إضافته كودجت في تخطيط صفحة. راجع قسم definePageLayout للتفاصيل.

عديم الرأس مقابل غير عديم الرأس

تأتي مكوّنات الواجهة الأمامية بوضعَي عرض يتحكّم بهما الخيار isHeadless:غير عديم الرأس (افتراضي) — يعرض المكوّن واجهة مستخدم مرئية. عند تشغيله من قائمة الأوامر يفتح في اللوحة الجانبية. هذا هو السلوك الافتراضي عندما تكون isHeadless تساوي false أو يتم تجاهلها.عديم الرأس (isHeadless: true) — يتم تركيب المكوّن بشكل غير مرئي في الخلفية. لا يفتح اللوحة الجانبية. تم تصميم المكوّنات عديمة الرأس لإجراءات تنفّذ منطقًا ثم تُزيل تركيبها ذاتيًا — على سبيل المثال، تشغيل مهمة غير متزامنة، أو الانتقال إلى صفحة، أو إظهار نافذة تأكيد منبثقة. تتوافق بشكل طبيعي مع مكوّنات Command في SDK الموصوفة أدناه.
src/front-components/sync-tracker.tsx
import { defineFrontComponent, useRecordId, enqueueSnackbar } from 'twenty-sdk';
import { useEffect } from 'react';

const SyncTracker = () => {
  const recordId = useRecordId();

  useEffect(() => {
    enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' });
  }, [recordId]);

  return null;
};

export default defineFrontComponent({
  universalIdentifier: '...',
  name: 'sync-tracker',
  description: 'Tracks record views silently',
  isHeadless: true,
  component: SyncTracker,
});
نظرًا لأن المكوّن يُرجع null، فإن Twenty يتخطّى عرض حاوية له — ولن تظهر مساحة فارغة في التخطيط. لا يزال لدى المكوّن إمكانية الوصول إلى جميع الخطافات وواجهة برمجة الاتصال مع المضيف.

مكوّنات Command في SDK

توفر حزمة twenty-sdk أربعة مكوّنات مساعدة من نوع Command مصممة للمكوّنات عديمة الرأس في الواجهة الأمامية. كل مكوّن ينفّذ إجراءً عند التركيب، ويتعامل مع الأخطاء بعرض إشعار Snackbar، ويزيل تركيب مكوّن الواجهة الأمامية تلقائيًا عند الانتهاء.استوردها من twenty-sdk/command:
  • Command — يشغّل رد نداء غير متزامن عبر الخاصية execute.
  • CommandLink — ينتقل إلى مسار في التطبيق. الخصائص: to، params، queryParams، options.
  • CommandModal — يفتح نافذة تأكيد منبثقة. إذا أكّد المستخدم، ينفّذ رد النداء execute. الخصائص: title، subtitle، execute، confirmButtonText، confirmButtonAccent.
  • CommandOpenSidePanelPage — يفتح صفحة محدّدة في اللوحة الجانبية. الخصائص: page، pageTitle، pageIcon.
فيما يلي مثال كامل لمكوّن واجهة أمامية عديم الرأس يستخدم Command لتشغيل إجراء من قائمة الأوامر:
src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk';
import { Command } from 'twenty-sdk/command';
import { CoreApiClient } from 'twenty-sdk/clients';

const RunAction = () => {
  const execute = async () => {
    const client = new CoreApiClient();

    await client.mutation({
      createTask: {
        __args: { data: { title: 'Created by my app' } },
        id: true,
      },
    });
  };

  return <Command execute={execute} />;
};

export default defineFrontComponent({
  universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
  name: 'run-action',
  description: 'Creates a task from the command menu',
  component: RunAction,
  isHeadless: true,
  command: {
    universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
    label: 'Run my action',
    icon: 'IconPlayerPlay',
  },
});
ومثال يستخدم CommandModal لطلب التأكيد قبل التنفيذ:
src/front-components/delete-draft.tsx
import { defineFrontComponent } from 'twenty-sdk';
import { CommandModal } from 'twenty-sdk/command';

const DeleteDraft = () => {
  const execute = async () => {
    // perform the deletion
  };

  return (
    <CommandModal
      title="Delete draft?"
      subtitle="This action cannot be undone."
      execute={execute}
      confirmButtonText="Delete"
      confirmButtonAccent="danger"
    />
  );
};

export default defineFrontComponent({
  universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
  name: 'delete-draft',
  description: 'Deletes a draft with confirmation',
  component: DeleteDraft,
  isHeadless: true,
  command: {
    universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
    label: 'Delete draft',
    icon: 'IconTrash',
  },
});

الوصول إلى سياق وقت التشغيل

داخل مكوّنك، استخدم خطافات SDK للوصول إلى المستخدم الحالي، والسجل، ومثيل المكوّن:
src/front-components/record-info.tsx
import {
  defineFrontComponent,
  useUserId,
  useRecordId,
  useFrontComponentId,
} from 'twenty-sdk';

const RecordInfo = () => {
  const userId = useUserId();
  const recordId = useRecordId();
  const componentId = useFrontComponentId();

  return (
    <div>
      <p>User: {userId}</p>
      <p>Record: {recordId ?? 'No record context'}</p>
      <p>Component: {componentId}</p>
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012',
  name: 'record-info',
  component: RecordInfo,
});
الخطافات المتاحة:
الخطّافالقيم المعادةالوصف
useUserId()string أو nullمعرّف المستخدم الحالي
useRecordId()string أو nullمعرّف السجل الحالي (عند وضعه على صفحة سجل)
useFrontComponentId()stringمعرّف مثيل هذا المكوّن
useFrontComponentExecutionContext(selector)يختلفالوصول إلى سياق التنفيذ الكامل عبر دالة محدِّد

واجهة الاتصال مع المضيف

يمكن للمكوّنات الأمامية تشغيل التنقّل والنوافذ المنبثقة والإشعارات باستخدام دوال من twenty-sdk:
دالةالوصف
navigate(to, params?, queryParams?, options?)الانتقال إلى صفحة داخل التطبيق
openSidePanelPage(params)فتح لوحة جانبية
closeSidePanel()إغلاق اللوحة الجانبية
openCommandConfirmationModal(params)عرض مربع حوار تأكيد
enqueueSnackbar(params)عرض إشعار توست
unmountFrontComponent()إلغاء تركيب المكوّن
updateProgress(progress)تحديث مؤشّر التقدّم
فيما يلي مثال يستخدم واجهة برمجة تطبيقات المضيف لعرض Snackbar وإغلاق اللوحة الجانبية بعد اكتمال الإجراء:
src/front-components/archive-record.tsx
import { defineFrontComponent, useRecordId } from 'twenty-sdk';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-sdk/clients';

const ArchiveRecord = () => {
  const recordId = useRecordId();

  const handleArchive = async () => {
    const client = new CoreApiClient();

    await client.mutation({
      updateTask: {
        __args: { id: recordId, data: { status: 'ARCHIVED' } },
        id: true,
      },
    });

    await enqueueSnackbar({
      message: 'Record archived',
      variant: 'success',
    });

    await closeSidePanel();
  };

  return (
    <div style={{ padding: '20px' }}>
      <p>Archive this record?</p>
      <button onClick={handleArchive}>Archive</button>
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
  name: 'archive-record',
  description: 'Archives the current record',
  component: ArchiveRecord,
});

خيارات الأوامر

إضافة حقل command إلى defineFrontComponent تُسجِّل المكوّن في قائمة الأوامر (Cmd+K). إذا كانت قيمة isPinned هي true، فسيظهر أيضًا كزر إجراء سريع في الزاوية العلوية اليمنى من الصفحة.
الحقلمطلوبالوصف
universalIdentifierنعممعرّف فريد ثابت للأمر
التسميةنعمالتسمية الكاملة المعروضة في قائمة الأوامر (Cmd+K)
shortLabelلاتسمية أقصر تُعرَض على زر الإجراء السريع المثبّت
أيقونةلااسم الأيقونة المعروض بجانب التسمية (مثل 'IconBolt' و'IconSend')
isPinnedلاعند كونها true، يعرض الأمر كزر إجراء سريع في الزاوية العلوية اليمنى من الصفحة
availabilityTypeلاتتحكّم في مكان ظهور الأمر: 'GLOBAL' (متاح دائمًا)، و'RECORD_SELECTION' (فقط عند تحديد سجلات)، أو 'FALLBACK' (يُعرَض عند عدم تطابق أي أوامر أخرى)
availabilityObjectUniversalIdentifierلاتقييد الأمر بصفحات نوع كائن معيّن (مثل سجلات Company فقط)
conditionalAvailabilityExpressionلاتعبير منطقي للتحكم ديناميكيًا في ما إذا كان الأمر مرئيًا (انظر أدناه)

تعابير الإتاحة الشرطية

يتيح لك الحقل conditionalAvailabilityExpression التحكّم في وقت ظهور الأمر بناءً على سياق الصفحة الحالي. استورد متغيّرات ومشغّلات مضبوطة الأنواع من twenty-sdk لبناء التعابير:
import {
  defineFrontComponent,
  pageType,
  numberOfSelectedRecords,
  objectPermissions,
  everyEquals,
  isDefined,
} from 'twenty-sdk';

export default defineFrontComponent({
  universalIdentifier: '...',
  name: 'bulk-action',
  component: BulkAction,
  command: {
    universalIdentifier: '...',
    label: 'Bulk Update',
    availabilityType: 'RECORD_SELECTION',
    conditionalAvailabilityExpression: everyEquals(
      objectPermissions,
      'canUpdateObjectRecords',
      true,
    ),
  },
});
متغيّرات السياق — تُمثّل الحالة الحالية للصفحة:
المتغيّرالنوعالوصف
pageTypestringنوع الصفحة الحالي (مثل 'RecordIndexPage' و'RecordShowPage')
isInSidePanelقيمة منطقيةما إذا كان المكوّن معروضًا في لوحة جانبية
numberOfSelectedRecordsرقمعدد السجلات المحدّدة حاليًا
isSelectAllقيمة منطقيةما إذا كان “تحديد الكل” مفعّلًا
selectedRecordsarrayكائنات السجلات المحدّدة
favoriteRecordIdsarrayمعرّفات السجلات المفضّلة
objectPermissionsالكائنالأذونات الخاصة بنوع الكائن الحالي
targetObjectReadPermissionsالكائنأذونات القراءة للكائن الهدف
targetObjectWritePermissionsالكائنأذونات الكتابة للكائن الهدف
featureFlagsالكائنأعلام الميزات المفعَّلة
objectMetadataItemالكائنبيانات التعريف لنوع الكائن الحالي
hasAnySoftDeleteFilterOnViewقيمة منطقيةما إذا كان العرض الحالي يحتوي على مرشّح حذف منطقي
المُشغِّلات — جمّع المتغيّرات في تعابير منطقية:
المُشغِّلالوصف
isDefined(value)true إذا لم تكن القيمة null/undefined
isNonEmptyString(value)true إذا كانت القيمة سلسلة غير فارغة
includes(array, value)true إذا كانت المصفوفة تحتوي على القيمة
includesEvery(array, prop, value)true إذا كانت خاصية كل عنصر تتضمن القيمة
every(array, prop)true إذا كانت الخاصية تُقيَّم بصحّة في كل عنصر
everyDefined(array, prop)true إذا كانت الخاصية معرّفة في كل عنصر
everyEquals(array, prop, value)true إذا كانت الخاصية تساوي القيمة في كل عنصر
some(array, prop)true إذا كانت الخاصية تُقيَّم بصحّة في عنصر واحد على الأقل
someDefined(array, prop)true إذا كانت الخاصية معرّفة في عنصر واحد على الأقل
someEquals(array, prop, value)true إذا كانت الخاصية تساوي القيمة في عنصر واحد على الأقل
someNonEmptyString(array, prop)true إذا كانت الخاصية سلسلة غير فارغة في عنصر واحد على الأقل
none(array, prop)true إذا كانت الخاصية تُقيَّم بخطأ في كل عنصر
noneDefined(array, prop)true إذا كانت الخاصية غير معرّفة في كل عنصر
noneEquals(array, prop, value)true إذا لم تكن الخاصية تساوي القيمة في أي عنصر

الأصول العامة

يمكن للمكوّنات الأمامية الوصول إلى ملفات من دليل public/ للتطبيق باستخدام getPublicAssetUrl:
import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk';

const Logo = () => <img src={getPublicAssetUrl('logo.png')} alt="Logo" />;

export default defineFrontComponent({
  universalIdentifier: '...',
  name: 'logo',
  component: Logo,
});
راجع قسم الأصول العامة للتفاصيل.

التنسيق

تدعم المكوّنات الأمامية عدة أساليب للتنسيق. يمكنك استخدام:
  • أنماط مضمنةstyle={{ color: 'red' }}
  • مكوّنات Twenty لواجهة المستخدم — استورد من twenty-sdk/ui (Button وTag وStatus وChip وAvatar وغيرها)
  • Emotion — CSS-in-JS مع @emotion/react
  • Styled-components — أنماط styled.div
  • Tailwind CSS — أصناف مساعدة
  • أي مكتبة CSS-in-JS متوافقة مع React
import { defineFrontComponent } from 'twenty-sdk';
import { Button, Tag, Status } from 'twenty-sdk/ui';

const StyledWidget = () => {
  return (
    <div style={{ padding: '16px', display: 'flex', gap: '8px' }}>
      <Button title="Click me" onClick={() => alert('Clicked!')} />
      <Tag text="Active" color="green" />
      <Status color="green" text="Online" />
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-567890123456',
  name: 'styled-widget',
  component: StyledWidget,
});
تُحدِّد المهارات تعليمات وإمكانات قابلة لإعادة الاستخدام يمكن لوكلاء الذكاء الاصطناعي استخدامها داخل مساحة العمل لديك. استخدم defineSkill() لتعريف مهارات مع تحقّق مدمج:
src/skills/example-skill.ts
import { defineSkill } from 'twenty-sdk';

export default defineSkill({
  universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
  name: 'sales-outreach',
  label: 'Sales Outreach',
  description: 'Guides the AI agent through a structured sales outreach process',
  icon: 'IconBrain',
  content: `You are a sales outreach assistant. When reaching out to a prospect:
1. Research the company and recent news
2. Identify the prospect's role and likely pain points
3. Draft a personalized message referencing specific details
4. Keep the tone professional but conversational`,
});
النقاط الرئيسية:
  • name هي سلسلة معرّف فريدة للمهارة (يُنصَح باستخدام kebab-case).
  • label هو اسم العرض المقروء للبشر الظاهر في واجهة المستخدم.
  • content يحتوي على تعليمات المهارة — وهو النص الذي يستخدمه وكيل الذكاء الاصطناعي.
  • icon (اختياري) يحدّد الأيقونة المعروضة في واجهة المستخدم.
  • description (اختياري) يوفّر سياقًا إضافيًا حول غرض المهارة.
الوكلاء هم مساعدون ذكاء اصطناعي يعيشون داخل مساحة العمل لديك. استخدم defineAgent() لإنشاء وكلاء بموجه نظام مخصّص:
src/agents/example-agent.ts
import { defineAgent } from 'twenty-sdk';

export default defineAgent({
  universalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
  name: 'sales-assistant',
  label: 'Sales Assistant',
  description: 'Helps the sales team draft outreach emails and research prospects',
  icon: 'IconRobot',
  prompt: 'You are a helpful sales assistant. Help users with their questions and tasks.',
});
النقاط الرئيسية:
  • name هي سلسلة معرّف فريدة للوكيل (يُنصح باستخدام kebab-case).
  • label هو اسم العرض الظاهر في واجهة المستخدم.
  • prompt هو موجه النظام الذي يحدّد سلوك الوكيل.
  • description (اختياري) يوفّر سياقًا حول ما يفعله الوكيل.
  • icon (اختياري) يحدّد الأيقونة المعروضة في واجهة المستخدم.
  • modelId (اختياري) يتجاوز نموذج الذكاء الاصطناعي الافتراضي الذي يستخدمه الوكيل.
العروض هي تكوينات محفوظة لكيفية عرض سجلات كائن ما — بما في ذلك الحقول المرئية وترتيبها وأي مرشّحات أو مجموعات مُطبَّقة. استخدم defineView() لتضمين عروض مُهيّأة مسبقًا مع تطبيقك:
src/views/example-view.ts
import { defineView, ViewKey } from 'twenty-sdk';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
import { NAME_FIELD_UNIVERSAL_IDENTIFIER } from '../objects/example-object';

export default defineView({
  universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
  name: 'All example items',
  objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
  icon: 'IconList',
  key: ViewKey.INDEX,
  position: 0,
  fields: [
    {
      universalIdentifier: 'f926bdb7-6af7-4683-9a09-adbca56c29f0',
      fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
      position: 0,
      isVisible: true,
      size: 200,
    },
  ],
});
النقاط الرئيسية:
  • objectUniversalIdentifier يحدّد الكائن الذي ينطبق عليه هذا العرض.
  • key يحدّد نوع العرض (مثل ViewKey.INDEX لعرض القائمة الرئيسي).
  • fields يتحكّم في الأعمدة الظاهرة وترتيبها. يشير كل حقل إلى fieldMetadataUniversalIdentifier.
  • يمكنك أيضًا تعريف filters وfilterGroups وgroups وfieldGroups لمزيد من التكوينات المتقدمة.
  • position يتحكّم في الترتيب عند وجود عدة عروض لنفس الكائن.
تضيف عناصر قائمة التنقل إدخالات مخصّصة إلى الشريط الجانبي لمساحة العمل. استخدم defineNavigationMenuItem() للارتباط بالعروض أو عناوين URL خارجية أو الكائنات:
src/navigation-menu-items/example-navigation-menu-item.ts
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk';
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from '../views/example-view';

export default defineNavigationMenuItem({
  universalIdentifier: '9327db91-afa1-41b6-bd9d-2b51a26efb4c',
  name: 'example-navigation-menu-item',
  icon: 'IconList',
  color: 'blue',
  position: 0,
  type: NavigationMenuItemType.VIEW,
  viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
});
النقاط الرئيسية:
  • type يحدّد إلى ماذا يرتبط عنصر القائمة: NavigationMenuItemType.VIEW لعرض محفوظ، أو NavigationMenuItemType.LINK لعنوان URL خارجي.
  • لروابط العروض، عيِّن viewUniversalIdentifier. لروابط خارجية، عيِّن link.
  • position يتحكّم في الترتيب ضمن الشريط الجانبي.
  • icon وcolor (اختياريان) يخصّصان المظهر.
تتيح لك تخطيطات الصفحات تخصيص مظهر صفحة تفاصيل السجل — ما الألسنة التي تظهر، وما الويدجتات داخل كل لسان، وكيف يتم ترتيبها. استخدم definePageLayout() لتضمين تخطيطات مخصّصة مع تطبيقك:
src/page-layouts/example-record-page-layout.ts
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';

export default definePageLayout({
  universalIdentifier: '203aeb94-6701-46d6-9af1-be2bbcc9e134',
  name: 'Example Record Page',
  type: 'RECORD_PAGE',
  objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
  tabs: [
    {
      universalIdentifier: '6ed26b60-a51d-4ad7-86dd-1c04c7f3cac5',
      title: 'Hello World',
      position: 50,
      icon: 'IconWorld',
      layoutMode: PageLayoutTabLayoutMode.CANVAS,
      widgets: [
        {
          universalIdentifier: 'aa4234e0-2e5f-4c02-a96a-573449e2351d',
          title: 'Hello World',
          type: 'FRONT_COMPONENT',
          configuration: {
            configurationType: 'FRONT_COMPONENT',
            frontComponentUniversalIdentifier:
              HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
          },
        },
      ],
    },
  ],
});
النقاط الرئيسية:
  • type يكون عادة 'RECORD_PAGE' لتخصيص عرض التفاصيل لكائن محدّد.
  • objectUniversalIdentifier يحدّد الكائن الذي ينطبق عليه هذا التخطيط.
  • يُعرّف كل tab قسمًا من الصفحة مع title وposition وlayoutMode (CANVAS لتخطيط حرّ).
  • يمكن لكل widget داخل لسان أن يعرض مكوّنًا أماميًا أو قائمة علاقات أو أنواع ويدجت مدمجة أخرى.
  • position على الألسنة يتحكّم في ترتيبها. استخدم قيمًا أعلى (مثل 50) لوضع الألسنة المخصّصة بعد الألسنة المدمجة.

الأصول العامة (مجلد public/)

يحتوي مجلد public/ في جذر تطبيقك على ملفات ثابتة — صور وأيقونات وخطوط وأي أصول أخرى يحتاجها تطبيقك وقت التشغيل. تُدرج هذه الملفات تلقائيًا في عمليات البناء، وتُزامَن أثناء وضع التطوير، وتُرفَع إلى الخادم. الملفات الموضوعة في public/ هي:
  • متاحة للعامة — بمجرد مزامنتها إلى الخادم، تُقدَّم الأصول عبر عنوان URL عام. لا يلزم توثيق للوصول إليها.
  • متاحة في المكوّنات الأمامية — استخدم عناوين الأصول لعرض الصور أو الأيقونات أو أي وسائط داخل مكوّنات React لديك.
  • متاحة في الدوال المنطقية — أشِر إلى عناوين الأصول في رسائل البريد الإلكتروني أو استجابات واجهات البرمجة أو أي منطق على جهة الخادم.
  • مستخدمة لبيانات تعريف السوق — يشير حقلا logoUrl وscreenshots في defineApplication() إلى ملفات من هذا المجلد (مثل public/logo.png). تُعرَض هذه عند نشر تطبيقك في السوق.
  • تُزامَن تلقائيًا في وضع التطوير — عند إضافة ملف في public/ أو تحديثه أو حذفه، تتم مزامنته إلى الخادم تلقائيًا. لا حاجة لإعادة التشغيل.
  • مضمَّنة في عمليات البناء — يقوم yarn twenty build بتجميع جميع الأصول العامة ضمن مخرجات التوزيع.

الوصول إلى الأصول العامة باستخدام getPublicAssetUrl

استخدم المساعد getPublicAssetUrl من twenty-sdk للحصول على العنوان الكامل لملف في دليل public/ لديك. يعمل ذلك في كلٍ من الدوال المنطقية والمكوّنات الأمامية. في دالة منطقية:
src/logic-functions/send-invoice.ts
import { defineLogicFunction, getPublicAssetUrl } from 'twenty-sdk';

const handler = async (): Promise<any> => {
  const logoUrl = getPublicAssetUrl('logo.png');
  const invoiceUrl = getPublicAssetUrl('templates/invoice.png');

  // Fetch the file content (no auth required — public endpoint)
  const response = await fetch(invoiceUrl);
  const buffer = await response.arrayBuffer();

  return { logoUrl, size: buffer.byteLength };
};

export default defineLogicFunction({
  universalIdentifier: 'a1b2c3d4-...',
  name: 'send-invoice',
  description: 'Sends an invoice with the app logo',
  timeoutSeconds: 10,
  handler,
});
في مكوّن أمامي:
src/front-components/company-card.tsx
import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk';

export default defineFrontComponent(() => {
  const logoUrl = getPublicAssetUrl('logo.png');

  return <img src={logoUrl} alt="App logo" />;
});
وسيطة path نسبية إلى مجلد public/ الخاص بتطبيقك. كلٌّ من getPublicAssetUrl('logo.png') وgetPublicAssetUrl('public/logo.png') يُحلاّن إلى العنوان نفسه — تتم إزالة بادئة public/ تلقائيًا إن وُجدت.

استخدام حِزَم npm

يمكنك تثبيت واستخدام أي حزمة npm في تطبيقك. يتم تجميع كلٍ من الدوال المنطقية والمكوّنات الأمامية باستخدام esbuild، والذي يُضمّن جميع التبعيات ضمن المخرجات — لا حاجة إلى node_modules وقت التشغيل.

تثبيت حزمة

yarn add axios
ثم استوردها في شيفرتك:
src/logic-functions/fetch-data.ts
import { defineLogicFunction } from 'twenty-sdk';
import axios from 'axios';

const handler = async (): Promise<any> => {
  const { data } = await axios.get('https://api.example.com/data');

  return { data };
};

export default defineLogicFunction({
  universalIdentifier: '...',
  name: 'fetch-data',
  description: 'Fetches data from an external API',
  timeoutSeconds: 10,
  handler,
});
وينطبق الأمر نفسه على المكوّنات الأمامية:
src/front-components/chart.tsx
import { defineFrontComponent } from 'twenty-sdk';
import { format } from 'date-fns';

const DateWidget = () => {
  return <p>Today is {format(new Date(), 'MMMM do, yyyy')}</p>;
};

export default defineFrontComponent({
  universalIdentifier: '...',
  name: 'date-widget',
  component: DateWidget,
});

كيف يعمل التجميع

تستخدم خطوة البناء أداة esbuild لإنتاج ملف واحد مستقل لكل دالة منطقية ولكل مكوّن أمامي. تُضمَّن جميع الحزم المستوردة داخل الحزمة. الدوال المنطقية تعمل في بيئة Node.js. الوحدات المدمجة في Node (fs وpath وcrypto وhttp وغيرها) متاحة ولا تحتاج إلى تثبيت. المكوّنات الأمامية تعمل ضمن Web Worker. وحدات Node المدمجة غير متاحة — المتاح فقط واجهات برمجة المتصفّح وحِزَم npm التي تعمل في بيئة المتصفّح. كلتا البيئتين تحتويان على twenty-client-sdk/core وtwenty-client-sdk/metadata كوحدات متاحة مُسبقًا — لا تُضمَّن هذه ضمن الحزم بل تُحلّ وقت التشغيل بواسطة الخادم.

توليد قوالب الكيانات باستخدام yarn twenty add

بدلًا من إنشاء ملفات الكيانات يدويًا، يمكنك استخدام أداة القوالب التفاعلية:
yarn twenty add
ستطالبك باختيار نوع الكيان وتُرشدك خلال الحقول المطلوبة. تُولّد ملفًا جاهزًا للاستخدام مع universalIdentifier ثابت واستدعاء defineEntity() الصحيح. يمكنك أيضًا تمرير نوع الكيان مباشرة لتخطي المطالبة الأولى:
yarn twenty add object
yarn twenty add logicFunction
yarn twenty add frontComponent

أنواع الكيانات المتاحة

نوع الكيانأمرالملف المُولَّد
كائنyarn twenty add objectsrc/objects/<name>.ts
الحقلyarn twenty add fieldsrc/fields/<name>.ts
دالة منطقيةyarn twenty add logicFunctionsrc/logic-functions/<name>.ts
مكوّن أماميyarn twenty add frontComponentsrc/front-components/<name>.tsx
دورyarn twenty add rolesrc/roles/<name>.ts
مهارةyarn twenty add skillsrc/skills/<name>.ts
وكيلyarn twenty add agentsrc/agents/<name>.ts
عرضyarn twenty add viewsrc/views/<name>.ts
عنصر قائمة التنقّلyarn twenty add navigationMenuItemsrc/navigation-menu-items/<name>.ts
تخطيط الصفحةyarn twenty add pageLayoutsrc/page-layouts/<name>.ts

ما الذي تُنشئه أداة القوالب

لكل نوع كيان قالب خاص به. على سبيل المثال، يسأل yarn twenty add object عن:
  1. الاسم (مفرد) — مثل invoice
  2. الاسم (جمع) — مثل invoices
  3. التسمية (مفرد) — تُستمد تلقائيًا من الاسم (مثل Invoice)
  4. التسمية (جمع) — تُملأ تلقائيًا (مثل Invoices)
  5. إنشاء عرض وعنصر تنقّل؟ — إذا أجبت بنعم، فستُنشئ أداة القوالب أيضًا عرضًا مطابقًا ورابط شريط جانبي للكائن الجديد.
أنواع الكيانات الأخرى لها مطالبات أبسط — فمعظمها يطلب اسمًا فقط. نوع الكيان field أكثر تفصيلاً: يطلب اسم الحقل وتسمية الحقل ونوعه (من قائمة بكل أنواع الحقول المتاحة مثل TEXT وNUMBER وSELECT وRELATION وغيرها)، ومعرّف universalIdentifier للكائن الهدف.

مسار خرج مخصّص

استخدم العلم --path لوضع الملف المُولَّد في موقع مخصّص:
yarn twenty add logicFunction --path src/custom-folder

عملاء واجهة برمجة تطبيقات مضبوطة الأنواع (twenty-client-sdk)

توفر حزمة twenty-client-sdk عميلين لـ GraphQL ذوي أنواع ثابتة للتفاعل مع واجهة Twenty البرمجية من وظائفك المنطقية ومكوّنات الواجهة الأمامية.
العميلاستيرادنقطة النهايةمُولَّد؟
CoreApiClienttwenty-client-sdk/core/graphql — بيانات مساحة العمل (السجلات، الكائنات)نعم، في وقت التطوير/البناء
MetadataApiClienttwenty-client-sdk/metadata/metadata — تكوين مساحة العمل، رفع الملفاتلا، يأتي مُجهزًا مسبقًا
CoreApiClient هو العميل الرئيسي للاستعلام وتعديل بيانات مساحة العمل. يُولَّد من مخطط مساحة العمل لديك أثناء yarn twenty dev أو yarn twenty build، لذا فهو مضبوط الأنواع بالكامل ليتوافق مع كائناتك وحقولك.
import { CoreApiClient } from 'twenty-client-sdk/core';

const client = new CoreApiClient();

// Query records
const { companies } = await client.query({
  companies: {
    edges: {
      node: {
        id: true,
        name: true,
        domainName: {
          primaryLinkLabel: true,
          primaryLinkUrl: true,
        },
      },
    },
  },
});

// Create a record
const { createCompany } = await client.mutation({
  createCompany: {
    __args: {
      data: {
        name: 'Acme Corp',
      },
    },
    id: true,
    name: true,
  },
});
يستخدم العميل صياغة مجموعة اختيار: مرِّر true لتضمين حقل، واستخدم __args للوسيطات، وعشّش الكائنات للعلاقات. ستحصل على إكمال تلقائي كامل وفحص للأنواع يعتمد على مخطط مساحة العمل لديك.
يتم توليد CoreApiClient في وقت التطوير/البناء. إذا استخدمته دون تشغيل yarn twenty dev أو yarn twenty build أولًا، فسيؤدي ذلك إلى خطأ. تحدث عملية التوليد تلقائيًا — إذ يستطلع CLI مخطط GraphQL لمساحة عملك وينشئ عميلًا مضبوط الأنواع باستخدام @genql/cli.

استخدام CoreSchema للتعليقات التوضيحية للأنواع

CoreSchema يوفّر أنواع TypeScript المطابقة لكائنات مساحة العمل لديك — مفيد لتعيين أنواع حالة المكوّن أو معاملات الدوال:
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
import { useState } from 'react';

const [company, setCompany] = useState<
  Pick<CoreSchema.Company, 'id' | 'name'> | undefined
>(undefined);

const client = new CoreApiClient();
const result = await client.query({
  company: {
    __args: { filter: { position: { eq: 1 } } },
    id: true,
    name: true,
  },
});
setCompany(result.company);
يأتي MetadataApiClient مُجهّزًا مسبقًا مع SDK (لا حاجة للتوليد). يستعلم عن نقطة النهاية /metadata للحصول على تكوين مساحة العمل والتطبيقات ورفع الملفات.
import { MetadataApiClient } from 'twenty-client-sdk/metadata';

const metadataClient = new MetadataApiClient();

// List first 10 objects in the workspace
const { objects } = await metadataClient.query({
  objects: {
    edges: {
      node: {
        id: true,
        nameSingular: true,
        namePlural: true,
        labelSingular: true,
        isCustom: true,
      },
    },
    __args: {
      filter: {},
      paging: { first: 10 },
    },
  },
});

رفع الملفات

يتضمن MetadataApiClient طريقة uploadFile لإرفاق الملفات بالحقول من نوع الملف:
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import * as fs from 'fs';

const metadataClient = new MetadataApiClient();

const fileBuffer = fs.readFileSync('./invoice.pdf');

const uploadedFile = await metadataClient.uploadFile(
  fileBuffer,                                         // file contents as a Buffer
  'invoice.pdf',                                      // filename
  'application/pdf',                                  // MIME type
  '58a0a314-d7ea-4865-9850-7fb84e72f30b',            // field universalIdentifier
);

console.log(uploadedFile);
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
المعلمةالنوعالوصف
fileBufferBufferالمحتوى الخام للملف
filenamestringاسم الملف (يُستخدم للتخزين والعرض)
contentTypestringنوع MIME (القيمة الافتراضية application/octet-stream إذا لم يُحدَّد)
fieldMetadataUniversalIdentifierstringقيمة universalIdentifier لحقل نوع الملف في كائنك
النقاط الرئيسية:
  • يستخدم universalIdentifier الخاص بالحقل (وليس معرّفه الخاص بمساحة العمل)، بحيث يعمل كود الرفع لديك عبر أي مساحة عمل مُثبَّت فيها تطبيقك.
  • العنوان url المُعاد هو عنوان URL موقّع يمكنك استخدامه للوصول إلى الملف المرفوع.
عند تشغيل كودك على Twenty (وظائف منطقية أو مكوّنات أمامية)، يقوم النظام الأساسي بحقن بيانات الاعتماد كمتغيرات بيئية:
  • TWENTY_API_URL — عنوان URL الأساسي لواجهة Twenty البرمجية
  • TWENTY_APP_ACCESS_TOKEN — مفتاح قصير العمر ذو نطاق يقتصر على الدور الافتراضي لوظيفة تطبيقك
لست بحاجة إلى تمرير هذه القيم إلى العملاء — فهي تُقرأ تلقائيًا من process.env. تُحدَّد أذونات مفتاح واجهة برمجة التطبيقات بواسطة الدور المشار إليه في defaultRoleUniversalIdentifier ضمن application-config.ts.

اختبار تطبيقك

يوفّر SDK واجهات برمجة قابلة للتنفيذ برمجيًا تمكّنك من بناء تطبيقك ونشره وتثبيته وإلغاء تثبيته من شيفرة الاختبار. بالاقتران مع Vitest وعملاء واجهة البرمجة مضبوطي الأنواع، يمكنك كتابة اختبارات تكامل تتحقّق من أن تطبيقك يعمل من البداية إلى النهاية مقابل خادم Twenty حقيقي.

إعداد

يتضمّن التطبيق المُولَّد بالقالب بالفعل Vitest. إذا أعددته يدويًا، فثبّت التبعيات:
yarn add -D vitest vite-tsconfig-paths
أنشئ vitest.config.ts في جذر تطبيقك:
vitest.config.ts
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';

export default defineConfig({
  plugins: [
    tsconfigPaths({
      projects: ['tsconfig.spec.json'],
      ignoreConfigErrors: true,
    }),
  ],
  test: {
    testTimeout: 120_000,
    hookTimeout: 120_000,
    include: ['src/**/*.integration-test.ts'],
    setupFiles: ['src/__tests__/setup-test.ts'],
    env: {
      TWENTY_API_URL: 'http://localhost:2020',
      TWENTY_API_KEY: 'your-api-key',
    },
  },
});
أنشئ ملف إعداد يتحقّق من إمكانية الوصول إلى الخادم قبل تشغيل الاختبارات:
src/__tests__/setup-test.ts
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { beforeAll } from 'vitest';

const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');

beforeAll(async () => {
  // Verify the server is running
  const response = await fetch(`${TWENTY_API_URL}/healthz`);

  if (!response.ok) {
    throw new Error(
      `Twenty server is not reachable at ${TWENTY_API_URL}. ` +
        'Start the server before running integration tests.',
    );
  }

  // Write a temporary config for the SDK
  fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });

  fs.writeFileSync(
    path.join(TEST_CONFIG_DIR, 'config.json'),
    JSON.stringify({
      remotes: {
        local: {
          apiUrl: process.env.TWENTY_API_URL,
          apiKey: process.env.TWENTY_API_KEY,
        },
      },
      defaultRemote: 'local',
    }, null, 2),
  );
});

واجهات SDK البرمجية

يُصدِّر المسار الفرعي twenty-sdk/cli دوالًا يمكنك استدعاؤها مباشرةً من شيفرة الاختبار:
دالةالوصف
appBuildبناء التطبيق واختياريًا حزم ملف tarball
appDeployرفع ملف tarball إلى الخادم
appInstallتثبيت التطبيق على مساحة العمل النشطة
appUninstallإلغاء تثبيت التطبيق من مساحة العمل النشطة
تُرجع كل دالة كائن نتيجة يحتوي على success: boolean وعلى إمّا data أو error.

كتابة اختبار تكامل

إليك مثالًا كاملًا يبني التطبيق وينشره ويثبّته، ثم يتحقّق من ظهوره في مساحة العمل:
src/__tests__/app-install.integration-test.ts
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

const APP_PATH = process.cwd();

describe('App installation', () => {
  beforeAll(async () => {
    const buildResult = await appBuild({
      appPath: APP_PATH,
      tarball: true,
      onProgress: (message: string) => console.log(`[build] ${message}`),
    });

    if (!buildResult.success) {
      throw new Error(`Build failed: ${buildResult.error?.message}`);
    }

    const deployResult = await appDeploy({
      tarballPath: buildResult.data.tarballPath!,
      onProgress: (message: string) => console.log(`[deploy] ${message}`),
    });

    if (!deployResult.success) {
      throw new Error(`Deploy failed: ${deployResult.error?.message}`);
    }

    const installResult = await appInstall({ appPath: APP_PATH });

    if (!installResult.success) {
      throw new Error(`Install failed: ${installResult.error?.message}`);
    }
  });

  afterAll(async () => {
    await appUninstall({ appPath: APP_PATH });
  });

  it('should find the installed app in the workspace', async () => {
    const metadataClient = new MetadataApiClient();

    const result = await metadataClient.query({
      findManyApplications: {
        id: true,
        name: true,
        universalIdentifier: true,
      },
    });

    const installedApp = result.findManyApplications.find(
      (app: { universalIdentifier: string }) =>
        app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
    );

    expect(installedApp).toBeDefined();
  });
});

تشغيل الاختبارات

تأكّد من تشغيل خادم Twenty المحلي لديك، ثم:
yarn test
أو في وضع المراقبة أثناء التطوير:
yarn test:watch

التحقق من الأنواع

يمكنك أيضًا تشغيل التحقق من الأنواع على تطبيقك دون تشغيل الاختبارات:
yarn twenty typecheck
يشغِّل هذا الأمر tsc --noEmit ويبلغ عن أي أخطاء في الأنواع.

مرجع CLI

بالإضافة إلى dev وbuild وadd وtypecheck، يوفّر CLI أوامر لتنفيذ الدوال وعرض السجلات وإدارة تثبيتات التطبيقات.

تنفيذ الدوال (yarn twenty exec)

تشغيل دالة منطقية يدويًا دون تشغيلها عبر HTTP أو cron أو حدث قاعدة بيانات:
# Execute by function name
yarn twenty exec -n create-new-post-card

# Execute by universalIdentifier
yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf

# Pass a JSON payload
yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}'

# Execute the post-install function
yarn twenty exec --postInstall

عرض سجلات الدوال (yarn twenty logs)

بثّ سجلات التنفيذ لدوال تطبيقك المنطقية:
# Stream all function logs
yarn twenty logs

# Filter by function name
yarn twenty logs -n create-new-post-card

# Filter by universalIdentifier
yarn twenty logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
يختلف هذا عن yarn twenty server logs، الذي يعرض سجلات حاوية Docker. يعرض yarn twenty logs سجلات تنفيذ دوال تطبيقك من خادم Twenty.

إلغاء تثبيت تطبيق (yarn twenty uninstall)

أزل تطبيقك من مساحة العمل النشطة:
yarn twenty uninstall

# Skip the confirmation prompt
yarn twenty uninstall --yes

إدارة الريموتات

الريموت هو خادم Twenty يتصل به تطبيقك. أثناء الإعداد، تُنشئ أداة إنشاء الهيكل واحدًا لك تلقائيًا. يمكنك إضافة ريموتات أخرى أو التبديل بينها في أي وقت.
# Add a new remote (opens a browser for OAuth login)
yarn twenty remote add

# Connect to a local Twenty server (auto-detects port 2020 or 3000)
yarn twenty remote add --local

# Add a remote non-interactively (useful for CI)
yarn twenty remote add --api-url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote

# List all configured remotes
yarn twenty remote list

# Switch the active remote
yarn twenty remote switch <name>
تُخزَّن بيانات اعتمادك في ~/.twenty/config.json.

التكامل المستمر (CI) باستخدام GitHub Actions

تولّد أداة إنشاء الهيكل سير عمل GitHub Actions جاهزًا للاستخدام في .github/workflows/ci.yml. يشغّل اختبارات التكامل لديك تلقائيًا عند كل دفع إلى main وعلى طلبات السحب. سير العمل:
  1. يجلب الشيفرة الخاصة بك
  2. يشغّل خادم Twenty مؤقتًا باستخدام الإجراء twentyhq/twenty/.github/actions/spawn-twenty-docker-image
  3. يثبّت التبعيات باستخدام yarn install --immutable
  4. يشغّل yarn test مع حقن TWENTY_API_URL وTWENTY_API_KEY من مخرجات الإجراء
.github/workflows/ci.yml
name: CI

on:
  push:
    branches:
      - main
  pull_request: {}

env:
  TWENTY_VERSION: latest

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Spawn Twenty instance
        id: twenty
        uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main
        with:
          twenty-version: ${{ env.TWENTY_VERSION }}
          github-token: ${{ secrets.GITHUB_TOKEN }}

      - name: Enable Corepack
        run: corepack enable

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
          cache: 'yarn'

      - name: Install dependencies
        run: yarn install --immutable

      - name: Run integration tests
        run: yarn test
        env:
          TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
          TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }}
لا تحتاج إلى تهيئة أي أسرار — إذ يبدأ إجراء spawn-twenty-docker-image خادم Twenty عابرًا مباشرة في المشغّل ويُخرِج تفاصيل الاتصال. يتم توفير السر GITHUB_TOKEN تلقائيًا من قِبل GitHub. لتثبيت إصدار محدّد من Twenty بدلًا من latest، غيّر متغير البيئة TWENTY_VERSION في أعلى سير العمل.