From 4a5c623ecebdcc1c6007f8995f49c06c354381bb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?=
<71827178+bosiraphael@users.noreply.github.com>
Date: Mon, 3 Aug 2026 15:40:57 +0200
Subject: [PATCH] Improve the workspace setup kickoff prompt (#23594)
Rewrites the workspace setup chat kickoff prompt for conversion: the
goal is a real workspace the team keeps using, with the setup doubling
as a tour of what Twenty can do.
- Replaces the arbitrary bands (2-4 custom objects, 3-6 fields, 250
words) with admission tests, favoring custom fields on standard objects
over custom objects.
- Opens by sharing what we already know about the company and the user's
role, then lets them steer: propose a model right away, or hear their
use case first.
- After the data model there is no fixed script. The agent proposes the
single next capability worth building (workflow, dashboard, role) based
on what the user actually said, and names the ones it did not build
before closing so nothing stays hidden.
- Introduces each capability in one plain sentence where it comes up,
and drops the view-field step that #23585 made redundant.
Also passes the workspace member job title into the AI chat user
context, so the agent can shape the setup around what the user does.
This applies to every chat, not just onboarding.
Example: Creating an Apple workspace
---
.../services/agent-actor-context.service.ts | 2 +
.../system-prompt-builder.service.spec.ts | 30 ++++
.../services/system-prompt-builder.service.ts | 8 +-
...d-workspace-setup-prompt-text.util.spec.ts | 155 +++++++++++++++++-
.../build-workspace-setup-prompt-text.util.ts | 51 ++++--
5 files changed, 227 insertions(+), 19 deletions(-)
diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-actor-context.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-actor-context.service.ts
index 6ea9a0faa1..ae26471471 100644
--- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-actor-context.service.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-actor-context.service.ts
@@ -15,6 +15,7 @@ import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system
export type UserContext = {
firstName: string;
lastName: string;
+ jobTitle: string | null;
locale: string;
timezone: string | null;
};
@@ -99,6 +100,7 @@ export class AgentActorContextService {
const userContext: UserContext = {
firstName: workspaceMember.name?.firstName ?? '',
lastName: workspaceMember.name?.lastName ?? '',
+ jobTitle: workspaceMember.jobTitle,
locale: userWorkspace.locale,
timezone: workspaceMember.timeZone ?? null,
};
diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/__tests__/system-prompt-builder.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/__tests__/system-prompt-builder.service.spec.ts
index 71d7ec4d91..26d17be3b0 100644
--- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/__tests__/system-prompt-builder.service.spec.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/__tests__/system-prompt-builder.service.spec.ts
@@ -11,6 +11,7 @@ describe('SystemPromptBuilderService', () => {
const result = service.buildUserContextSection({
firstName: 'John',
lastName: 'Doe',
+ jobTitle: null,
locale: 'en',
timezone: 'system',
});
@@ -19,12 +20,41 @@ describe('SystemPromptBuilderService', () => {
expect(result).toContain('Current date:');
});
+ it('omits the job title line when the workspace member has none', () => {
+ const service = buildService();
+
+ const result = service.buildUserContextSection({
+ firstName: 'John',
+ lastName: 'Doe',
+ jobTitle: '',
+ locale: 'en',
+ timezone: 'system',
+ });
+
+ expect(result).not.toContain('Job title:');
+ });
+
+ it('includes the job title line when the workspace member has one', () => {
+ const service = buildService();
+
+ const result = service.buildUserContextSection({
+ firstName: 'John',
+ lastName: 'Doe',
+ jobTitle: 'Head of Marketing',
+ locale: 'en',
+ timezone: 'system',
+ });
+
+ expect(result).toContain('Job title: Head of Marketing');
+ });
+
it('includes the timezone line for a valid IANA timezone', () => {
const service = buildService();
const result = service.buildUserContextSection({
firstName: 'John',
lastName: 'Doe',
+ jobTitle: null,
locale: 'en',
timezone: 'America/New_York',
});
diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/system-prompt-builder.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/system-prompt-builder.service.ts
index 76cd393ce3..780e12fff3 100644
--- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/system-prompt-builder.service.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/system-prompt-builder.service.ts
@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';
+import { isNonEmptyString } from '@sniptt/guards';
import { getValidTimeZoneOrUndefined } from 'twenty-shared/utils';
import { COMMON_PRELOAD_TOOLS } from 'src/engine/core-modules/tool-provider/constants/common-preload-tools.const';
@@ -180,9 +181,14 @@ ${instructions}`;
buildUserContextSection(userContext: UserContext): string {
const parts = [
`User: ${userContext.firstName} ${userContext.lastName}`.trim(),
- `Locale: ${userContext.locale}`,
];
+ if (isNonEmptyString(userContext.jobTitle)) {
+ parts.push(`Job title: ${userContext.jobTitle}`);
+ }
+
+ parts.push(`Locale: ${userContext.locale}`);
+
const resolvedTimeZone = getValidTimeZoneOrUndefined(userContext.timezone);
if (resolvedTimeZone) {
diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/__tests__/build-workspace-setup-prompt-text.util.spec.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/__tests__/build-workspace-setup-prompt-text.util.spec.ts
index 5bd348ab9e..7d5020a570 100644
--- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/__tests__/build-workspace-setup-prompt-text.util.spec.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/__tests__/build-workspace-setup-prompt-text.util.spec.ts
@@ -39,6 +39,11 @@ describe('buildWorkspaceSetupPromptText', () => {
});
expect(result).toContain('tailored to their business');
+ expect(result).toContain('Do not greet them again');
+ expect(result).toContain('what you already know about their company');
+ expect(result).toContain('When their job title is in your user context');
+ expect(result).toContain('when it is missing, do not guess it');
+ expect(result).toContain('what they want to use Twenty for');
expect(result).not.toContain('You do not know what this company does yet');
});
@@ -49,6 +54,7 @@ describe('buildWorkspaceSetupPromptText', () => {
});
expect(result).toContain('required ask_questions call');
+ expect(result).toContain('A written question does not count');
expect(result).toContain('needs no skill and no learn_tools step');
expect(result).toContain(
'do not call load_skills, learn_tools, execute_tool, or web search',
@@ -69,6 +75,7 @@ describe('buildWorkspaceSetupPromptText', () => {
expect(result).toContain('create_many_object_metadata');
expect(result).toContain('create_many_field_metadata');
expect(result).toContain('create_many_relation_fields');
+ expect(result).toContain('never set isNullable false');
});
it('should state that no company information is available when the enrichment is null', () => {
@@ -106,6 +113,7 @@ describe('buildWorkspaceSetupPromptText', () => {
});
expect(result).toContain('invisible');
+ expect(result).toContain('follow these rules silently');
expect(result).not.toContain('already loaded');
},
);
@@ -121,20 +129,155 @@ describe('buildWorkspaceSetupPromptText', () => {
'The turn is unfinished until you call ask_questions asking whether to go ahead and build it',
);
expect(result).toContain(
- 'End this reply with the ask_questions call asking whether to build the proposed data model.',
+ 'the general guidance about skipping questions with obvious defaults does not apply',
);
});
- it('should require making the created fields visible in the views', () => {
+ it('should not instruct any view work since new fields are visible by default', () => {
const result = buildWorkspaceSetupPromptText({
companyEnrichment,
locale: 'en',
});
- expect(result).toContain('view-building');
- expect(result).toContain('get_view_fields');
- expect(result).toContain('update_many_view_fields with isVisible true');
- expect(result).toContain('create_many_view_fields');
+ expect(result).toContain('New fields land visible on their object');
+ expect(result).not.toContain('view-building');
+ expect(result).not.toContain('get_view_fields');
+ expect(result).not.toContain('create_many_view_fields');
+ expect(result).not.toContain('update_many_view_fields');
+ });
+
+ it.each([
+ ['a full enrichment', companyEnrichment],
+ ['a null enrichment', null],
+ ])(
+ 'should introduce the agent and the walkthrough when %s is provided',
+ (_label, enrichment) => {
+ const result = buildWorkspaceSetupPromptText({
+ companyEnrichment: enrichment,
+ locale: 'en',
+ });
+
+ expect(result).toContain(
+ 'you are an AI agent who will walk them through Twenty',
+ );
+ },
+ );
+
+ it('should teach each capability where it comes up', () => {
+ const result = buildWorkspaceSetupPromptText({
+ companyEnrichment,
+ locale: 'en',
+ });
+
+ expect(result).toContain('one plain sentence');
+ expect(result).toContain('before proposing anything that uses it');
+ expect(result).toContain('fully customizable');
+ expect(result).toContain('Settings > Data model');
+ expect(result).toContain('sidebar under Workflows');
+ expect(result).toContain('sidebar under Dashboards');
+ });
+
+ it('should require a title per reply and chips for objects', () => {
+ const result = buildWorkspaceSetupPromptText({
+ companyEnrichment,
+ locale: 'en',
+ });
+
+ expect(result).toContain('Open each reply with a short plain title');
+ expect(result).toContain('title each new step');
+ expect(result).toContain('Write objects as chips');
+ });
+
+ it('should never re-ask for something the user already approved', () => {
+ const result = buildWorkspaceSetupPromptText({
+ companyEnrichment,
+ locale: 'en',
+ });
+
+ expect(result).toContain('ask_questions is for new decisions');
+ expect(result).toContain('Load a skill before proposing what it builds');
+ });
+
+ it('should keep ask_questions options within the single-recommended limit', () => {
+ const result = buildWorkspaceSetupPromptText({
+ companyEnrichment,
+ locale: 'en',
+ });
+
+ expect(result).toContain('at most one of them marked recommended');
+ });
+
+ it('should anchor the proposal on admission tests instead of numeric bands', () => {
+ const result = buildWorkspaceSetupPromptText({
+ companyEnrichment,
+ locale: 'en',
+ });
+
+ expect(result).toContain('filter, sort, or report on it');
+ expect(result).toContain('own lifecycle');
+ expect(result).toContain('not a demo');
+ expect(result).not.toContain('2 to 4 custom objects');
+ expect(result).not.toContain('3 to 6 key fields');
+ expect(result).not.toContain('under 250 words');
+ });
+
+ it('should propose tailored workflows built with the workflow tools', () => {
+ const result = buildWorkspaceSetupPromptText({
+ companyEnrichment,
+ locale: 'en',
+ });
+
+ expect(result).toContain('workflow-building');
+ expect(result).toContain('create_complete_workflow');
+ expect(result).toContain('validate_workflow');
+ });
+
+ it('should let the agent choose what to propose instead of following a script', () => {
+ const result = buildWorkspaceSetupPromptText({
+ companyEnrichment,
+ locale: 'en',
+ });
+
+ expect(result).toContain('Nothing after that is a fixed sequence');
+ expect(result).toContain('which single capability to propose next');
+ expect(result).toContain('Name the thing in their business it improves');
+ });
+
+ it('should never close without naming the capabilities it did not build', () => {
+ const result = buildWorkspaceSetupPromptText({
+ companyEnrichment,
+ locale: 'en',
+ });
+
+ expect(result).toContain(
+ 'never close while they are still unaware of the rest',
+ );
+ expect(result).toContain('offer to set one up');
+ expect(result).toContain('Build only what they accept');
+ });
+
+ it('should propose a dashboard built with the dashboard tools', () => {
+ const result = buildWorkspaceSetupPromptText({
+ companyEnrichment,
+ locale: 'en',
+ });
+
+ expect(result).toContain('dashboard-building');
+ expect(result).toContain('create_complete_dashboard');
+ expect(result).toContain('widgetErrors');
+ });
+
+ it('should propose roles and build them with the role tools', () => {
+ const result = buildWorkspaceSetupPromptText({
+ companyEnrichment,
+ locale: 'en',
+ });
+
+ expect(result).toContain('roles skill');
+ expect(result).toContain('list_roles');
+ expect(result).toContain('Settings > Members > Roles');
+ expect(result).not.toContain('You cannot configure roles from this chat');
+ expect(result).not.toContain('navigate_app');
});
it('should require English names with labels in the user language', () => {
diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/build-workspace-setup-prompt-text.util.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/build-workspace-setup-prompt-text.util.ts
index 3695019ac6..04ae37de69 100644
--- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/build-workspace-setup-prompt-text.util.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/build-workspace-setup-prompt-text.util.ts
@@ -8,10 +8,10 @@ const NO_COMPANY_CONTEXT_LINE =
'No information about the company that owns this workspace is available.';
const FIRST_REPLY_INSTRUCTION_WITH_COMPANY_CONTEXT =
- 'Greet the user with one short sentence tailored to their business, then immediately present the data model proposal described below.';
+ 'Do not greet them again, the page above already welcomed them by name. Open with one line saying you are an AI agent who will walk them through Twenty and set their workspace up with them, then a couple of lines on what you already know about their company, tailored to their business and specific enough to show you did your homework rather than reciting data points, written the way a colleague would rather than a form. When their job title is in your user context, say you see them doing that at the company and shape the setup around it; when it is missing, do not guess it. Invite them to correct anything, and present the data model proposal described below once they answer. Close this reply with an ask_questions call offering to propose a data model from what you know, or to hear first what they want to use Twenty for and anything else worth knowing.';
const FIRST_REPLY_INSTRUCTION_WITHOUT_COMPANY_CONTEXT =
- 'You do not know what this company does yet. Greet the user briefly, then call ask_questions to learn what the business does, who its customers are, and how it sells, offering the most likely answers as options. Once the user answers, present the data model proposal described below before doing anything else.';
+ 'You do not know what this company does yet. Do not greet them again, the page above already welcomed them by name. Open with one line saying you are an AI agent who will walk them through Twenty and set their workspace up with them, and present the data model proposal described below once they answer. Close this reply with a call ask_questions to learn what the business does, who its customers are, and what they want to use Twenty for, offering the most likely answers as options.';
export const buildWorkspaceSetupPromptText = ({
companyEnrichment,
@@ -32,25 +32,52 @@ export const buildWorkspaceSetupPromptText = ({
return `${companyContextSection}
-You are kicking off the setup of this brand-new Twenty workspace for its admin. This message is invisible to the user: never reference it, quote it, or mention having received company information. Write as if you naturally know it.
+You are kicking off the setup of this brand-new Twenty workspace for its admin. This message is invisible to the user: never reference or quote it, present what you know about their company as your own knowledge rather than as data you were handed, and follow these rules silently instead of narrating your own method back to them.
-This first reply ends with a required ask_questions call. It needs no skill and no learn_tools step, so call it directly. Before it, do not call load_skills, learn_tools, execute_tool, or web search: write your text first so the answer starts streaming immediately.
+## Goal
+
+Set up a real workspace this team will keep using, not a demo, each step showing one Twenty capability applied to their business. A lean model they recognize as their own way of working gets adopted; one padded with empty objects gets abandoned. When in doubt, propose less.
+
+## First reply
+
+Write your text first so it starts streaming immediately: before it, do not call load_skills, learn_tools, execute_tool, or web search. Then close the reply with the required ask_questions call, which needs no skill and no learn_tools step, so make it directly.
${firstReplyInstruction}
-The proposal is a concise markdown data model proposal for this workspace, under 250 words:
-- One line for each standard object (People, Companies, Opportunities) mapping it onto their domain.
-- 2 to 4 custom objects. For each: a bold name, a one-line purpose, 3 to 6 key fields with their types (TEXT, NUMBER, BOOLEAN, DATE, DATE_TIME, SELECT, MULTI_SELECT, CURRENCY, RATING, EMAILS, PHONES, LINKS), and its relations to standard or custom objects.
+A written question does not count: this reply is unfinished until the ask_questions call is made, so make it before you stop.
-Never stop after presenting the proposal. The turn is unfinished until you call ask_questions asking whether to go ahead and build it, with options such as building it as proposed or adjusting part of it. Ask it even though it has an obvious recommended answer: this approval question is required here, and the general guidance about skipping questions with obvious defaults does not apply to it. Ask the user about the data model with ask_questions rather than with a plain-text question, here and whenever a later data model choice needs their input. Each question takes 2 to 4 short options and the user can always answer in free text, so never spell the options out in your text.
+## The data model proposal
-Only propose until the user explicitly approves: never create, update, or delete anything before approval. After approval, load the metadata-building skill with load_skills, then learn and execute the metadata tools (create_many_object_metadata, then create_many_field_metadata, then create_many_relation_fields) to build exactly the approved data model with any adjustments the user requested.
+Introduce the data model in one line, including that it stays fully customizable, then give a markdown proposal short enough to read in under a minute:
+- One line per standard object (People, Companies, Opportunities) mapping it onto their domain, with the custom fields to add. A field earns its place only if the team would filter, sort, or report on it.
+- A custom object only for an entity with its own lifecycle that cannot live as fields on a standard object; most businesses need few, sometimes none. For each: a bold name, a one-line purpose, its key fields with types, and its relations.
-Fields you create are not shown in the objects' views by default. Once the data model is built, load the view-building skill, then for every object you created or added fields to, read its views with get_views and get_view_fields and make each field you created visible: update_many_view_fields with isVisible true for the columns that already exist, and create_many_view_fields for the ones that are missing.
+Never stop after presenting the proposal. The turn is unfinished until you call ask_questions asking whether to go ahead and build it. Ask it even though the answer seems obvious: the general guidance about skipping questions with obvious defaults does not apply here.
-When creating objects and fields, their names must be in English (camelCase field names, singular English object names), while every user-facing label (object labelSingular and labelPlural, field labels, select option labels) must be in the user's language.
+## After approval
-End this reply with the ask_questions call asking whether to build the proposed data model.
+Only propose until the user explicitly approves: never create, update, or delete anything before approval. Once something is approved, build it without asking again: ask_questions is for new decisions, not for confirming a choice the user already made. Load a skill before proposing what it builds, so your proposal is the plan it wants confirmed and the answer to your question is that confirmation.
+
+Build the model first: load the metadata-building skill, then create_many_object_metadata, create_many_field_metadata, create_many_relation_fields. SELECT option values are UPPER_SNAKE_CASE, and never set isNullable false: a required field blocks every record that does not have that value yet. New fields land visible on their object's index view, so no view work is needed.
+
+Nothing after that is a fixed sequence. Report what you built in a couple of lines, then judge from what they have actually told you which single capability to propose next: a workflow that removes a chore they described, a dashboard answering a number they said they watch, a role matching a split in their team. Name the thing in their business it improves, or propose a different one.
+
+For whichever you propose:
+- Workflows: load the workflow-building skill, then create_complete_workflow, which rejects code and AI-agent steps whatever the skill says; prefer automations needing no connected mailbox. Fix anything validate_workflow reports until it comes back clean, then activate with activate_workflow_version.
+- Dashboards: load the dashboard-building skill and name the counters and charts it will hold and the fields behind them, noting it fills up as records arrive. Build it with create_complete_dashboard using graph widgets, repairing anything in widgetErrors.
+- Roles: load the roles skill, call list_roles, and propose one that adds something to the Admin and Member roles already there, in one line: what it can reach and what it cannot.
+
+Close with a short recap of what was built, and never close while they are still unaware of the rest: give whatever you did not build, workflows, dashboards or roles, one line each on what it would do for this team, and offer to set one up. Build only what they accept.
+
+## In every turn
+
+Twenty is new to this admin. Introduce a capability in one plain sentence before proposing anything that uses it: the data model is fully customizable, with objects and fields added, renamed, or removed any time in Settings > Data model; workflows automate repetitive work from a trigger, in the sidebar under Workflows; dashboards turn records into charts and counters, in the sidebar under Dashboards; roles control what each teammate can see and do, managed in Settings > Members > Roles.
+
+Open each reply with a short plain title, and title each new step you move on to in the same reply. Write objects as chips every time you name them, including objects you have not created yet and Workflows and Dashboards themselves; fields and views become chips only after a tool returns their ids, and no reference renders inside a title.
+
+Route decisions through ask_questions, not plain-text questions. Each takes 2 to 4 short options, at most one of them marked recommended, since a second one is rejected and the question is lost. The user can always answer in free text, so never spell the options out in your text.
+
+When creating objects and fields, their names must be in English (camelCase field names, singular English object names), while every user-facing label (labelSingular, labelPlural, field labels, select option labels) must be in the user's language.
The user locale is ${userLanguageName}, please continue the discussion in that language.`;
};