Skip to content

Commit 1b4e49c

Browse files
authored
Merge branch 'main' into precheck-google
2 parents 89d8e8e + 58e8b46 commit 1b4e49c

File tree

486 files changed

+9600
-4884
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

486 files changed

+9600
-4884
lines changed

packages/create-twenty-app/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ In interactive mode, you can pick from:
8989
- **Example front component** — a React UI component (`front-components/hello-world.tsx`)
9090
- **Example view** — a saved view for the example object (`views/example-view.ts`)
9191
- **Example navigation menu item** — a sidebar link (`navigation-menu-items/example-navigation-menu-item.ts`)
92+
- **Example skill** — an AI agent skill definition (`skills/example-skill.ts`)
9293

9394
## What gets scaffolded
9495

@@ -106,11 +107,12 @@ In interactive mode, you can pick from:
106107
- `front-components/hello-world.tsx` — Example front component
107108
- `views/example-view.ts` — Example saved view for the example object
108109
- `navigation-menu-items/example-navigation-menu-item.ts` — Example sidebar navigation link
110+
- `skills/example-skill.ts` — Example AI agent skill definition
109111

110112
## Next steps
111113
- Run `yarn twenty help` to see all available commands.
112114
- Use `yarn twenty auth:login` to authenticate with your Twenty workspace.
113-
- Explore the generated project and add your first entity with `yarn twenty entity:add` (logic functions, front components, objects, roles, views, navigation menu items).
115+
- Explore the generated project and add your first entity with `yarn twenty entity:add` (logic functions, front components, objects, roles, views, navigation menu items, skills).
114116
- Use `yarn twenty app:dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
115117
- Types are auto‑generated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated`.
116118

packages/create-twenty-app/src/create-app.command.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export class CreateAppCommand {
114114
includeExampleFrontComponent: false,
115115
includeExampleView: false,
116116
includeExampleNavigationMenuItem: false,
117+
includeExampleSkill: false,
117118
};
118119
}
119120

@@ -125,6 +126,7 @@ export class CreateAppCommand {
125126
includeExampleFrontComponent: true,
126127
includeExampleView: true,
127128
includeExampleNavigationMenuItem: true,
129+
includeExampleSkill: true,
128130
};
129131
}
130132

@@ -164,6 +166,11 @@ export class CreateAppCommand {
164166
value: 'navigationMenuItem',
165167
checked: true,
166168
},
169+
{
170+
name: 'Example skill (AI agent skill definition)',
171+
value: 'skill',
172+
checked: true,
173+
},
167174
],
168175
},
169176
]);
@@ -189,6 +196,7 @@ export class CreateAppCommand {
189196
includeExampleView: includeView,
190197
includeExampleNavigationMenuItem:
191198
selectedExamples.includes('navigationMenuItem'),
199+
includeExampleSkill: selectedExamples.includes('skill'),
192200
};
193201
}
194202

packages/create-twenty-app/src/types/scaffolding-options.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ export type ExampleOptions = {
77
includeExampleFrontComponent: boolean;
88
includeExampleView: boolean;
99
includeExampleNavigationMenuItem: boolean;
10+
includeExampleSkill: boolean;
1011
};

packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import { type ExampleOptions } from '@/types/scaffolding-options';
2+
import { GENERATED_DIR } from 'twenty-shared/application';
3+
import { copyBaseApplicationProject } from '@/utils/app-template';
14
import * as fs from 'fs-extra';
2-
import { join } from 'path';
35
import { tmpdir } from 'os';
4-
import { copyBaseApplicationProject } from '@/utils/app-template';
5-
import { type ExampleOptions } from '@/types/scaffolding-options';
6+
import { join } from 'path';
67

78
// Mock fs-extra's copy function to skip copying base template (not available during tests)
89
jest.mock('fs-extra', () => {
@@ -23,11 +24,13 @@ const ALL_EXAMPLES: ExampleOptions = {
2324
includeExampleFrontComponent: true,
2425
includeExampleView: true,
2526
includeExampleNavigationMenuItem: true,
27+
includeExampleSkill: true,
2628
};
2729

2830
const NO_EXAMPLES: ExampleOptions = {
2931
includeExampleObject: false,
3032
includeExampleField: false,
33+
includeExampleSkill: false,
3134
includeExampleLogicFunction: false,
3235
includeExampleFrontComponent: false,
3336
includeExampleView: false,
@@ -109,7 +112,7 @@ describe('copyBaseApplicationProject', () => {
109112

110113
const gitignoreContent = await fs.readFile(gitignorePath, 'utf8');
111114
expect(gitignoreContent).toContain('/node_modules');
112-
expect(gitignoreContent).toContain('generated');
115+
expect(gitignoreContent).toContain(GENERATED_DIR);
113116
});
114117

115118
it('should create yarn.lock file', async () => {
@@ -437,6 +440,7 @@ describe('copyBaseApplicationProject', () => {
437440
exampleOptions: {
438441
includeExampleObject: false,
439442
includeExampleField: false,
443+
includeExampleSkill: false,
440444
includeExampleLogicFunction: false,
441445
includeExampleFrontComponent: true,
442446
includeExampleView: false,
@@ -472,6 +476,7 @@ describe('copyBaseApplicationProject', () => {
472476
appDirectory: testAppDirectory,
473477
exampleOptions: {
474478
includeExampleObject: false,
479+
includeExampleSkill: false,
475480
includeExampleField: false,
476481
includeExampleLogicFunction: true,
477482
includeExampleFrontComponent: false,

packages/create-twenty-app/src/utils/app-template.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,14 @@ export const copyBaseApplicationProject = async ({
8989
});
9090
}
9191

92+
if (exampleOptions.includeExampleSkill) {
93+
await createExampleSkill({
94+
appDirectory: sourceFolderPath,
95+
fileFolder: 'skills',
96+
fileName: 'example-skill.ts',
97+
});
98+
}
99+
92100
await createDefaultPostInstallFunction({
93101
appDirectory: sourceFolderPath,
94102
fileFolder: 'logic-functions',
@@ -424,6 +432,36 @@ export default defineNavigationMenuItem({
424432
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
425433
};
426434

435+
const createExampleSkill = async ({
436+
appDirectory,
437+
fileFolder,
438+
fileName,
439+
}: {
440+
appDirectory: string;
441+
fileFolder?: string;
442+
fileName: string;
443+
}) => {
444+
const universalIdentifier = v4();
445+
446+
const content = `import { defineSkill } from 'twenty-sdk';
447+
448+
export const EXAMPLE_SKILL_UNIVERSAL_IDENTIFIER =
449+
'${universalIdentifier}';
450+
451+
export default defineSkill({
452+
universalIdentifier: EXAMPLE_SKILL_UNIVERSAL_IDENTIFIER,
453+
name: 'example-skill',
454+
label: 'Example Skill',
455+
description: 'A sample skill for your application',
456+
icon: 'IconBrain',
457+
content: 'Add your skill instructions here. Skills provide context and capabilities to AI agents.',
458+
});
459+
`;
460+
461+
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
462+
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
463+
};
464+
427465
const createApplicationConfig = async ({
428466
displayName,
429467
description,

packages/twenty-docs/developers/extend/capabilities/apps.mdx

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Apps let you build and manage Twenty customizations **as code**. Instead of conf
1414
**What you can do today:**
1515
- Define custom objects and fields as code (managed data model)
1616
- Build logic functions with custom triggers
17+
- Define skills for AI agents
1718
- Deploy the same app across multiple workspaces
1819

1920
## Prerequisites
@@ -44,7 +45,7 @@ yarn twenty app:dev
4445
The scaffolder supports three modes for controlling which example files are included:
4546

4647
```bash filename="Terminal"
47-
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item)
48+
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill)
4849
npx create-twenty-app@latest my-app
4950

5051
# Minimal: only core files (application-config.ts and default-role.ts)
@@ -117,8 +118,10 @@ my-twenty-app/
117118
│ └── hello-world.tsx # Example front component
118119
├── views/
119120
│ └── example-view.ts # Example saved view definition
120-
└── navigation-menu-items/
121-
└── example-navigation-menu-item.ts # Example sidebar navigation link
121+
├── navigation-menu-items/
122+
│ └── example-navigation-menu-item.ts # Example sidebar navigation link
123+
└── skills/
124+
└── example-skill.ts # Example AI agent skill definition
122125
```
123126

124127
With `--minimal`, only the core files are created (`application-config.ts`, `roles/default-role.ts`, and `logic-functions/post-install.ts`). With `--interactive`, you choose which example files to include.
@@ -147,6 +150,7 @@ The SDK detects entities by parsing your TypeScript files for **`export default
147150
| `defineField()` | Field extensions for existing objects |
148151
| `defineView()` | Saved view definitions |
149152
| `defineNavigationMenuItem()` | Navigation menu item definitions |
153+
| `defineSkill()` | AI agent skill definitions |
150154

151155
<Note>
152156
**File naming is flexible.** Entity detection is AST-based — the SDK scans your source files for the `export default define<Entity>({...})` pattern. You can organize your files and folders however you like. Grouping by entity type (e.g., `logic-functions/`, `roles/`) is just a convention for code organization, not a requirement.
@@ -167,7 +171,7 @@ export default defineObject({
167171
Later commands will add more files and folders:
168172

169173
- `yarn twenty app:dev` will auto-generate a typed API client in `node_modules/twenty-sdk/generated` (typed Twenty client + workspace types).
170-
- `yarn twenty entity:add` will add entity definition files under `src/` for your custom objects, functions, front components, or roles.
174+
- `yarn twenty entity:add` will add entity definition files under `src/` for your custom objects, functions, front components, roles, skills, and more.
171175

172176
## Authentication
173177

@@ -220,6 +224,7 @@ The SDK provides helper functions for defining your app entities. As described i
220224
| `defineField()` | Extend existing objects with additional fields |
221225
| `defineView()` | Define saved views for objects |
222226
| `defineNavigationMenuItem()` | Define sidebar navigation links |
227+
| `defineSkill()` | Define AI agent skills |
223228

224229
These functions validate your configuration at build time and provide IDE autocompletion and type safety.
225230

@@ -729,6 +734,40 @@ You can create new front components in two ways:
729734
- **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new front component.
730735
- **Manual**: Create a new `*.front-component.tsx` file and use `defineFrontComponent()`.
731736

737+
### Skills
738+
739+
Skills define reusable instructions and capabilities that AI agents can use within your workspace. Use `defineSkill()` to define skills with built-in validation:
740+
741+
```typescript
742+
// src/skills/example-skill.ts
743+
import { defineSkill } from 'twenty-sdk';
744+
745+
export default defineSkill({
746+
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
747+
name: 'sales-outreach',
748+
label: 'Sales Outreach',
749+
description: 'Guides the AI agent through a structured sales outreach process',
750+
icon: 'IconBrain',
751+
content: `You are a sales outreach assistant. When reaching out to a prospect:
752+
1. Research the company and recent news
753+
2. Identify the prospect's role and likely pain points
754+
3. Draft a personalized message referencing specific details
755+
4. Keep the tone professional but conversational`,
756+
});
757+
```
758+
759+
Key points:
760+
- `name` is a unique identifier string for the skill (kebab-case recommended).
761+
- `label` is the human-readable display name shown in the UI.
762+
- `content` contains the skill instructions — this is the text the AI agent uses.
763+
- `icon` (optional) sets the icon displayed in the UI.
764+
- `description` (optional) provides additional context about the skill's purpose.
765+
766+
You can create new skills in two ways:
767+
768+
- **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new skill.
769+
- **Manual**: Create a new file and use `defineSkill()`, following the same pattern.
770+
732771
### Generated typed client
733772

734773
The typed client is auto-generated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated` based on your workspace schema. Use it in your functions:
@@ -754,6 +793,51 @@ Notes:
754793
- The API key's permissions are determined by the role referenced in your `application-config.ts` via `defaultRoleUniversalIdentifier`. This is the default role used by logic functions of your application.
755794
- Applications can define roles to follow least‑privilege. Grant only the permissions your functions need, then point `defaultRoleUniversalIdentifier` to that role's universal identifier.
756795

796+
#### Uploading files
797+
798+
The generated `Twenty` client includes an `uploadFile` method for attaching files to file-type fields on your workspace objects. Because standard GraphQL clients do not support multipart file uploads natively, the client provides this dedicated method that implements the [GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec) under the hood.
799+
800+
```typescript
801+
import Twenty from '~/generated';
802+
import * as fs from 'fs';
803+
804+
const client = new Twenty();
805+
806+
const fileBuffer = fs.readFileSync('./invoice.pdf');
807+
808+
const uploadedFile = await client.uploadFile(
809+
fileBuffer, // file contents as a Buffer
810+
'invoice.pdf', // filename
811+
'application/pdf', // MIME type (defaults to 'application/octet-stream')
812+
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universal identifier
813+
);
814+
815+
console.log(uploadedFile);
816+
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
817+
```
818+
819+
The method signature:
820+
821+
```typescript
822+
uploadFile(
823+
fileBuffer: Buffer,
824+
filename: string,
825+
contentType: string,
826+
fieldMetadataUniversalIdentifier: string,
827+
): Promise<{ id: string; path: string; size: number; createdAt: string; url: string }>
828+
```
829+
830+
| Parameter | Type | Description |
831+
|-----------|------|-------------|
832+
| `fileBuffer` | `Buffer` | The raw file contents |
833+
| `filename` | `string` | The name of the file (used for storage and display) |
834+
| `contentType` | `string` | MIME type of the file (defaults to `application/octet-stream` if omitted) |
835+
| `fieldMetadataUniversalIdentifier` | `string` | The `universalIdentifier` of the file-type field on your object |
836+
837+
Key points:
838+
- The method sends the file to the **metadata endpoint** (not the main GraphQL endpoint), where the upload mutation is resolved.
839+
- It uses the field's `universalIdentifier` (not its workspace-specific ID), so your upload code works across any workspace where your app is installed — consistent with how apps reference fields everywhere else.
840+
- The returned `url` is a signed URL you can use to access the uploaded file.
757841

758842
### Hello World example
759843

0 commit comments

Comments
 (0)