Refined demo workspace creation skill (rebased, review fixes) (#19274)

## Summary

Rebased version of #19051 with all review comments addressed. Clean
branch on latest main, lint/typecheck/tests passing.

### Changes from original PR
- AI can now create, update, and delete **view filters**
(`ViewFilterToolsFactory`) and **view sorts** (`ViewSortToolsFactory`)
- `create_view` now accepts `calendarFieldName`, `calendarLayout`, and
`fieldNames` to configure views at creation time
- Three new standard skills: `view-building`, `view-filters-and-sorts`,
`custom-objects-cleanup`
- `workspace-demo-seeding` skill reworked to keep standard objects and
enrich them with custom fields
- Cache invalidation for nav menu items when object `isActive` changes
- Dashboard tool descriptions improved (RECORD_TABLE widget workflow)

### Review comments addressed (all 10 from #19051)
1. **Sentry + Cubic**: Calendar field DATE/DATE_TIME validation — added
`resolveCalendarFieldMetadataId` using `isFieldMetadataDateKind`
2. **Cubic**: "navigate tool" → "navigate_app tool" in skill metadata
(all 7 occurrences)
3. **Copilot**: KANBAN views now require `mainGroupByFieldName` — throws
clear error if missing
4. **Copilot**: CALENDAR views now require both `calendarFieldName` and
`calendarLayout` — validated before DB call
5. **Copilot**: Mock field fixtures include `type` property (DATE_TIME,
TEXT, SELECT)
6. **Copilot**: `ViewFilterValue` type assertion instead of unsafe `as
string` casts (3 locations)
7. **FelixMalfait**: Removed
`NavigationMenuItemObjectDeactivationListener` — replaced with cache
invalidation
8. **FelixMalfait**: Consolidated `ViewFilterToolProvider` and
`ViewSortToolProvider` into single `ViewToolProvider`
9. Removed `VIEW_FILTER` and `VIEW_SORT` from `ToolCategory` enum
(merged into `VIEW`)
10. Removed stale `existingFeatureFlagsMap` param incompatible with
current main

## Test plan
- [x] `npx nx lint:diff-with-main twenty-server` — passes
- [x] `npx nx typecheck twenty-server` — passes
- [x] `view-tools.factory.spec.ts` — all 20 tests pass (including 3 new
validation tests)

Supersedes #19051

https://claude.ai/code/session_01QPV74NU6vzmJb32e4i899E

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Félix Malfait
2026-04-03 09:19:46 +02:00
committed by GitHub
parent 12b031b67d
commit bb3d556799
20 changed files with 1689 additions and 360 deletions
@@ -32,6 +32,15 @@ export const STANDARD_SKILL = {
'workspace-demo-seeding': {
universalIdentifier: '20202020-c81b-4af8-9255-4c34bd0eac9c',
},
'view-building': {
universalIdentifier: '20202020-e4a2-4b3f-9c71-d8f6a2b51e3a',
},
'view-filters-and-sorts': {
universalIdentifier: '20202020-f5b3-4c4e-8d82-e9a7b3c62f4b',
},
'custom-objects-cleanup': {
universalIdentifier: '20202020-a1d3-4e5f-b6c7-8d9e0f1a2b3c',
},
} as const satisfies Record<
string,
{
@@ -149,39 +149,60 @@ Prioritize data integrity and provide clear feedback on operations performed.`,
'Seeding demo metadata and data for workspace setup and testing purposes',
icon: 'IconDatabase',
content: `# Workspace Demo Seeding Skill
You will create a demo workspace that fits a particular type of company given by the user.
You will transform the existing standard workspace into a fully custom demo tailored to the user's business type.
Do not ask the user for more information, just be creative with the objects and fields, but stay professional and coherent.
The goal is to tell a coherent and realistic story with the data: custom fields added to standard objects, new custom objects for domain-specific entities, rich relations, seeded and updated records, views, and enrichment data (emails, calendar events, tasks, notes, files) that make the workspace feel like a real company in operation.
The goal is to be able to tell a coherent and realistic story with the data, so for example if the user says it is a car repair shop,
we can create objects for cars, employees, repairs, customers, and the relevant relations between them,
and then we can seed data that tells the story of the car repair shop, for example we can create a customer, then create a car for that customer, then create a repair for that car, and so on.
We should end up with a dashboard that shows the relevant metrics for the car repair shop, for example the number of repairs, the revenue, the most common car brands, and so on.
## Object strategy
Create relations fields between objects, for example a car repair shop workspace would have objects for cars, employees, repairs, customers, and the relevant relations between them.
**Keep the standard objects — People, Companies, and Opportunities — and reuse their existing seed data.** They already have emails and calendar events linked to them as participants. The demo story is built on top of them, not instead of them.
Create 5 to 7 objects, with 5 to 8 fields each, and the relevant relations between them.
- **People** → map to the domain's "contact" role (e.g. clients, candidates, customers, agents)
- **Companies** → map to the domain's "organisation" role (e.g. suppliers, agencies, employers)
- **Opportunities** → map to the domain's "deal/pipeline" role (e.g. job applications, deals, repair estimates)
**Add 2 to 3 additional custom objects** for domain-specific entities that don't map to People/Companies/Opportunities (e.g. Properties, Cars, Products, Projects). Keep the custom object count low — the standard objects carry most of the story.
**Add custom fields** to People, Companies, and Opportunities to enrich them with domain-specific data (e.g. add "specialisation", "licenseNumber" to People; add "industry vertical", "tier" to Companies; add "stage", "closeDate" to Opportunities).
Create rich relation fields between standard and custom objects to show off the relational capabilities of the platform.
If you have to create multiple things you *MUST* use the relevant create many tool if it exists:
- Use *create_many_object_metadata* to create all objects at once
- Use *create_many_field_metadata* to create all non-relation fields at once
- Use *create_many_object_metadata* to create all custom objects at once
- Use *create_many_field_metadata* to create all non-relation fields at once (including new fields on standard objects)
- Use *create_many_relation_fields* to create all relation fields between objects at once (do this AFTER creating the objects and non-relation fields)
If you have to wait use the navigate tool.
If you have to wait use the navigate_app tool.
For the fields you will create, make sure to create a good variety of field types to showcase the different capabilities of the platform, for example:
- Create SELECT and SELECT_MULTIPLE field types for building demo board index views and table with groups views
- Create DATE_TIME fields to be able to create calendar views
- Create CURRENCY and NUMERIC fields for graphs
*Here are the steps to follow closely :*
*Here are the steps to follow closely:*
STEP 1: Create all the objects at once with create_many_object_metadata, DO THIS FIRST
STEP 0: Present a plan to the user and wait for approval.
- Use list_object_metadata_items to see all available objects in the workspace
- Use find_people (limit: 5) and find_companies (limit: 5) and find_opportunities (limit: 5) to understand the existing seed data shape
- Based on the user's business type, propose a plan that lists:
- How People, Companies, and Opportunities map to the domain story (e.g. "People = Candidates", "Companies = Employers")
- The 23 custom objects you will create, each with a one-line description of their role
- The custom fields you will add to People, Companies, and Opportunities
- A brief description of the key relations between objects
- Present this plan to the user and *wait for their confirmation or adjustments* before proceeding
- Once approved, call the custom-objects-cleanup skill to archive all existing custom objects without asking for user confirmation
- Wait 3 seconds after deletions for the backend side effects to be completed
STEP 1: Create all the custom objects at once with create_many_object_metadata
name must start with lowercase letter and contain only alphanumeric letters
STEP 2: Wait 3 seconds, for the backend side effects to be completed
STEP 3: Create all NON-RELATION fields for ALL objects by batch with create_many_field_metadata, do a batch for each object.
STEP 3: Create all NON-RELATION fields for ALL objects by batch with create_many_field_metadata.
Do a separate batch call for each object.
This includes:
- New custom fields for the standard objects (Person, Company, Opportunity) — use their objectMetadataId from list_object_metadata_items
- All non-relation fields for the new custom objects
DO NOT include relation fields in this step. Only create TEXT, NUMBER, BOOLEAN, DATE_TIME, SELECT, MULTI_SELECT, CURRENCY, etc.
SELECT option values must be UPPER_SNAKE_CASE
@@ -193,25 +214,100 @@ targetFieldIcon is like IconSomething, it's ok if it doesn't exist in the icon l
STEP 6: Wait 3 seconds, for the backend side effects to be completed
STEP 7: For each object:
- Navigate the object's default view USE THE NAVIGATE TOOL
STEP 7: Rename and enrich the first N records of People, Companies, and Opportunities.
- Use find_people (limit: 50, orderBy: [{ position: "AscNullsFirst" }]), find_companies (limit: 50, orderBy: [{ position: "AscNullsFirst" }]), find_opportunities (limit: 50, orderBy: [{ position: "AscNullsFirst" }]) to get the IDs of the first records in each table
- Ordering by position ascending gives the earliest-inserted records, which are contiguous in the table — this keeps the demo data tightly grouped and makes the workspace feel coherent
- For each standard object, call update_people / update_companies / update_opportunities **individually per record** (one call per record) to set domain-relevant names and field values:
- **People**: replace nameFirstName + nameLastName with realistic names that fit the domain role (e.g. for a law firm: "Sophie Martin", "James O'Brien"; for a clinic: "Dr. Clara Reyes", "Marco Bianchi"). Also set jobTitle to a domain-appropriate title.
- **Companies**: replace name with realistic company names that fit the domain (e.g. for a law firm: "Ashford & Partners", "Nexus Legal Group"; for a clinic: "Meridian Health Clinic", "CarePoint Medical").
- **Opportunities**: replace name with a domain-relevant deal name (e.g. "Q2 retainer — Ashford & Partners", "New patient intake — Meridian Health").
- Also set the new custom fields on each record: spread realistic values across SELECT fields, set plausible CURRENCY/NUMERIC amounts, set DATE_TIME fields around TODAY.
- Do this one record at a time — the API does not support bulk individual updates with different values per record
- Wait 3 seconds after finishing all updates for one object type before moving to the next
STEP 7.5: Add view fields to the default views of standard objects to expose the new custom fields.
For each of People, Companies, and Opportunities:
- Navigate to the object's default view using the navigate_app tool
- Wait 3 seconds
- Use create_many_view_fields to add all the new custom fields to the default view so they are visible
- Use decimal positions between 0 and 1 to insert them right after the label identifier field
- Navigate to the object's default view again using the navigate_app tool so the user can see the enriched records
- Wait 3 seconds
STEP 8: For each new custom object, repeat ALL of the following sub-steps before moving to the next object:
- Navigate the object's default view using the navigate_app tool
- Wait 3 seconds, so the user has time to see the object default view
- Create the view fields for the default view, use the create_many_view_fields tool, and make sure to include all created fields, including the relation fields, so that we have a complete view of the object with all its fields.
BE CAREFUL to use a position that will put those view fields right after the first label identifier field
which has a position of 0 and the next system created fields which begin at 1, *so use decimal positions between 0 and 1*
*YOU MUST CREATE ALL VIEW FIELDS FOR ALL FIELDS, INCLUDING RELATION FIELDS, IN THIS STEP, DO NOT LEAVE ANY FIELD WITHOUT A VIEW FIELD, OTHERWISE IT WILL NOT BE VISIBLE IN THE DEFAULT VIEW AND THE USER WON'T KNOW IT EXISTS*
BE CAREFUL to use a position that will put those view fields right after the first label identifier field
which has a position of 0 and the next system created fields which begin at 1, *so use decimal positions between 0 and 1*
*YOU MUST CREATE ALL VIEW FIELDS FOR ALL FIELDS, INCLUDING RELATION FIELDS, IN THIS STEP, DO NOT LEAVE ANY FIELD WITHOUT A VIEW FIELD, OTHERWISE IT WILL NOT BE VISIBLE IN THE DEFAULT VIEW AND THE USER WON'T KNOW IT EXISTS*
- Then seed relevant and realistic mock data as we said earlier :
- **MANDATORY**: Navigate to the object's default view again using the navigate_app tool — YOU MUST DO THIS BEFORE EACH OBJECT'S DATA SEEDING, every single time, without exception
- Wait 3 seconds
- Seed relevant and realistic mock data for this object:
- use the relevant tool to create many records for this object
- between 20 and 50
- with a coherent combination of values
- link records to existing People and Companies using the relation fields you created
- use dates that are around TODAY so it's relevant for seeing past / future and present records
Loop STEP 7 for all the objects
- **MANDATORY**: Navigate to the object's default view again using the navigate_app tool so the user can see the populated data — DO NOT SKIP THIS, even if you already navigated earlier in this loop iteration
- Wait 3 seconds so the user has time to see the seeded records
STEP 8 : After you've finished with this part, let's proceed to the dashboard creation. We will create a dashboard with 4 graphs.
- Create a new dashboard with a relevant story to showcase the data you've just created, for example if it's a car repair shop, you can create a dashboard that shows the number of repairs per month, the revenue per month, the most common car brands, and the distribution of repairs by employee.
- Navigate to the dashboard page
- Then create 2 to 3 additional views for this object, one at a time. For each view, complete ALL of the following sub-steps before creating the next view:
- Create the view using the create_view tool:
- If the object has a SELECT field (e.g. status, stage, priority, type), create a **KANBAN** view grouped by that SELECT field with a relevant name like "By Status", "Pipeline", "By Priority".
- Set kanbanAggregateOperation to COUNT so each column shows the number of records.
- If there is a CURRENCY or NUMERIC field, also set kanbanAggregateOperationFieldName to that field for a SUM aggregate view.
- If the object has a DATE or DATE_TIME field (e.g. dueDate, closedAt, scheduledAt), create a **CALENDAR** view and pass both \`calendarFieldName\` (that field name) and \`calendarLayout\` ("DAY", "WEEK", or "MONTH") with a relevant name like "Calendar", "Schedule", "Timeline".
- Create a **TABLE** view with a meaningful group (mainGroupByFieldName set to a SELECT field) with a name like "By Type", "By Stage", "Grouped", or similar.
- Use create_many_view_fields to add all relevant field columns to this view (using decimal positions between 0 and 1)
- Add filters and sorts to this view:
- **KANBAN views**: Sort by a CURRENCY or NUMERIC field DESC (biggest value first) if one exists, or by createdAt DESC. Add a filter to exclude archived/cancelled records if such a SELECT option exists.
- **CALENDAR views**: Sort by the date field ASC (earliest events first). Add a filter using IS_IN_FUTURE or IS_RELATIVE to show only upcoming records by default.
- **TABLE with groups**: Sort by createdAt DESC (most recent first) and add a filter on a meaningful field (e.g. status IS_NOT "CANCELLED", or amount GREATER_THAN_OR_EQUAL to some threshold that keeps ~80% of the records visible).
- **MANDATORY**: Navigate to this view immediately using the navigate_app tool — YOU MUST DO THIS FOR EVERY SINGLE VIEW, right after its fields/filters/sorts are set up, without exception
- Wait 3 seconds so the user can see the view and course-correct if needed
Also create additional views for the standard objects (People, Companies, Opportunities) that showcase the new custom fields:
- For People: a KANBAN view grouped by the new SELECT field you added (e.g. "By Specialisation", "By Status")
- For Opportunities: a KANBAN view grouped by the new stage/status field (pipeline view)
- For Companies: a TABLE view grouped by the new SELECT field
Navigate to each view after creating it. Wait 3 seconds.
Loop STEP 8 for all the custom objects
STEP 9: Create a multi-tab dashboard that tells the full story of the business.
Use create_complete_dashboard to create the first tab, then add_dashboard_tab + add_dashboard_widget for subsequent tabs.
**Structure: 3 tabs**
Tab 1 — "Overview": high-level KPIs and charts across the whole workspace
- Row 0: 34 AGGREGATE_CHART widgets (KPIs) — one per key metric (e.g. total revenue from Opportunities, count of active People, count of open deals). columnSpan 34, rowSpan 3.
- Row 3: 12 BAR_CHART or LINE_CHART widgets showing trends over time (group by a DATE_TIME field with MONTH granularity). columnSpan 6, rowSpan 7.
- Row 3: 1 PIE_CHART showing distribution by a SELECT field (e.g. status, type). columnSpan 6, rowSpan 7.
- Row 10: 1 STANDALONE_RICH_TEXT widget summarising the dashboard story. columnSpan 12, rowSpan 3.
Tab 2 — "[Domain object] pipeline" (e.g. "Deals", "Applications", "Repairs"): focus on Opportunities enriched with domain data
- Before adding the RECORD_TABLE widget, run this 3-step sequence:
1. create_view (type TABLE, name e.g. "Active Deals") → get the new viewId
2. create_many_view_fields on the new viewId — add 46 key fields (name, the new stage/status SELECT, a CURRENCY/NUMERIC field, a DATE field, linked Person or Company). Use positions 0, 1, 2… and isVisible: true.
3. create_many_view_filters + create_view_sort — e.g. filter out CLOSED/LOST records (SELECT IS_NOT "CLOSED"), sort by value DESC
- Row 0: 1 RECORD_TABLE widget. Set objectMetadataId to Opportunity, configuration.viewId to the dedicated view. columnSpan 12, rowSpan 8.
- Row 8: 1 BAR_CHART grouped by the stage SELECT field. columnSpan 6, rowSpan 7.
- Row 8: 1 PIE_CHART or AGGREGATE_CHART on the CURRENCY field. columnSpan 6, rowSpan 7.
Tab 3 — "[Domain people role] list" (e.g. "Clients", "Candidates", "Contacts"): focus on People enriched with domain data
- Before adding the RECORD_TABLE widget, run this 3-step sequence:
1. create_view (type TABLE, name e.g. "All Clients") → get the new viewId
2. create_many_view_fields — add 45 key fields (name, email, the new SELECT/status field, a DATE field, linked Company)
3. create_view_sort — sort by createdAt DESC or by name ASC
- Row 0: 1 RECORD_TABLE widget with the dedicated view. columnSpan 12, rowSpan 8.
- Row 8: 23 AGGREGATE_CHART KPIs (count, totals). columnSpan 4, rowSpan 3.
- Row 11: 1 BAR_CHART or LINE_CHART. columnSpan 12, rowSpan 7.
After creating the dashboard, navigate to the dashboard page.
`,
isCustom: false,
},
@@ -295,6 +391,15 @@ You help users create and manage dashboards with widgets.
- STANDALONE_RICH_TEXT: configurationType "STANDALONE_RICH_TEXT" + body with markdown content
- IMPORTANT: Put the actual text content in configuration.body.markdown, NOT in the widget title
- Widget title should be a short label (e.g. "Notes", "Summary"), body.markdown holds the real content
- RECORD_TABLE: configurationType "RECORD_TABLE" — displays a filterable, sortable record list
- **MANDATORY 3-step pre-sequence before creating the widget**:
1. call create_view (type TABLE, name e.g. "Repairs Dashboard Table") → get the new viewId
2. call create_many_view_fields on the new viewId — add 46 of the most relevant fields (label identifier + key SELECT/DATE/CURRENCY fields). Use positions 0, 1, 2… and isVisible: true.
3. call create_many_view_filters and/or create_view_sort on the new viewId to focus the table (e.g. filter out DONE/CANCELLED records, sort by createdAt DESC or a date field ASC)
- Never reuse a record index view — widget views and record index views must be separate
- Set objectMetadataId on the widget (top-level, required)
- Set configuration.viewId to the UUID of the dedicated view (required)
- columnSpan 12 (full width) or 6 (half width), rowSpan 610
Example (STANDALONE_RICH_TEXT):
{
@@ -302,12 +407,26 @@ Example (STANDALONE_RICH_TEXT):
"body": { "markdown": "## Quarterly Summary\\n\\nKey metrics:\\n- Revenue up 15%\\n- 42 new deals closed\\n\\n**Next steps**: Focus on enterprise pipeline." }
}
Example (RECORD_TABLE — always run the 3-step pre-sequence first):
Step 1 — create_view: { "name": "Active Repairs", "objectNameSingular": "repair", "type": "TABLE" } → { "id": "<view-uuid>" }
Step 2 — create_many_view_fields: { "viewFields": [{ "viewId": "<view-uuid>", "fieldMetadataId": "<status-field-uuid>", "position": 1, "isVisible": true }, { "viewId": "<view-uuid>", "fieldMetadataId": "<amount-field-uuid>", "position": 2, "isVisible": true }] }
Step 3 — create_many_view_filters: { "filters": [{ "viewId": "<view-uuid>", "fieldMetadataId": "<status-field-uuid>", "operand": "IS_NOT", "value": "DONE" }] }
Step 3b — create_view_sort: { "viewId": "<view-uuid>", "fieldMetadataId": "<createdAt-field-uuid>", "direction": "DESC" }
Step 4 — add_dashboard_widget: { "type": "RECORD_TABLE", "objectMetadataId": "<repair-object-uuid>", "configuration": { "configurationType": "RECORD_TABLE", "viewId": "<view-uuid>" }, "gridPosition": { "row": 0, "column": 0, "rowSpan": 8, "columnSpan": 12 } }
## Tabs
Use add_dashboard_tab to create multiple tabs in a dashboard. Each tab has its own set of widgets.
Good tab structure: one overview tab (KPIs + charts) + one or more detail tabs (RECORD_TABLE + focused charts).
After creating a tab, use its returned tabId as pageLayoutTabId when calling add_dashboard_widget.
## Grid System
- 12 columns (0-11)
- KPI widgets: rowSpan 2-4, columnSpan 3-4
- Charts: rowSpan 6-8, columnSpan 6-12
- Common layouts: 4 KPIs in a row (columnSpan 3), 2 charts side by side (columnSpan 6), full width chart (columnSpan 12)
- Record tables: rowSpan 6-10, columnSpan 6-12 (full-width preferred)
- Common layouts: 4 KPIs in a row (columnSpan 3), 2 charts side by side (columnSpan 6), full width chart or table (columnSpan 12)
## Best Practices
@@ -315,7 +434,8 @@ Example (STANDALONE_RICH_TEXT):
- Group related charts together
- Use consistent heights within rows
- Start simple, add complexity as needed
- When modifying a chart, confirm whether the user wants to change settings or change chart type`,
- When modifying a chart, confirm whether the user wants to change settings or change chart type
- Use RECORD_TABLE widgets to give users direct access to filtered record lists without leaving the dashboard`,
isCustom: false,
},
}),
@@ -973,6 +1093,262 @@ para.paragraph_format.space_after = Pt(12)
},
}),
'view-building': (args: Omit<CreateStandardSkillArgs, 'context'>) =>
createStandardSkillFlatMetadata({
...args,
context: {
skillName: 'view-building',
name: 'view-building',
label: 'View Building',
description:
'Creating and configuring views (table, board/kanban, calendar) for objects to organize and visualize records',
icon: 'IconLayoutBoard',
content: `# View Building Skill
You help users create and configure views to organize how they see their records.
## View Types
- **TABLE**: Standard table/grid view. Works for any object. Default view type.
- **KANBAN**: Board view grouped by a SELECT field. Best for pipeline/status-based workflows.
- **CALENDAR**: Calendar view using a DATE or DATE_TIME field. Best for time-based records.
## Tools
- get_views - List existing views (filter by object name)
- create_view - Create a new view
- update_view - Update view name/icon
- delete_view - Delete a view
- create_many_view_fields - Add visible columns to a view
- update_many_view_fields - Update column configuration
- get_view_fields - List columns in a view
- list_object_metadata_items - Discover objects and their fields
- navigate_app - Navigate to a view after creation
## Workflow
1. **Identify the target object**: If the user didn't specify which object, ask them. Present available objects and explain what each holds:
- **Company**: Business accounts (name, domain, employees, revenue, address)
- **Person**: Contacts (name, email, phone, job title, company)
- **Opportunity**: Pipeline deals (name, stage, amount, close date, company, contact)
- **Task**: Action items (title, status, due date, assignee)
- **Note**: Free-form notes (title, body)
- Plus any custom objects in the workspace
2. **Choose the view type**: Suggest the best type based on the object's data:
- TABLE: Good default for any object, great for browsing large datasets
- KANBAN: Ideal when objects have a SELECT field representing stages/statuses (e.g., Opportunity → stage, Task → status)
- CALENDAR: Ideal when objects have DATE/DATE_TIME fields (e.g., Opportunity → closeDate, Task → dueAt)
3. **Create the view**: Use create_view with the right parameters.
- For KANBAN: The mainGroupByFieldName is required — ask user which SELECT field to group by, or suggest the most natural one.
- For CALENDAR: You must provide both \`calendarFieldName\` (a DATE/DATE_TIME field name) and \`calendarLayout\` ("DAY", "WEEK", or "MONTH") when calling create_view.
- For TABLE: No special configuration needed.
4. **Configure view fields**: Use create_many_view_fields to add relevant columns. Choose fields that make sense for the view's purpose. Use decimal positions between 0 and 1 to place them after the label identifier field.
5. **Navigate**: Use navigate_app to show the user their new view.
## KANBAN Best Practices
- The grouping field must be a SELECT type
- Common groupings: Opportunity by stage, Task by status
- Optionally set kanbanAggregateOperation (COUNT, SUM, AVG, MIN, MAX) and kanbanAggregateOperationFieldName for column summaries
- Example: Sum of amount per stage for Opportunity board
## CALENDAR Best Practices
- Requires a DATE or DATE_TIME field on the object
- Best for: Opportunity close dates, Task due dates, any event-based data
## TABLE with Groups
- TABLE views can also be grouped by a field using mainGroupByFieldName
- This creates collapsible sections in the table, organized by the grouping field values
- Works with SELECT fields for categorical grouping
## Approach
- If the user is vague (e.g., "create a board"), ask which object they want to see
- Suggest the most relevant view type based on the object's fields
- After creating a view, always configure useful view fields and navigate to it
- Explain what each view type does so users can make informed choices`,
isCustom: false,
},
}),
'view-filters-and-sorts': (args: Omit<CreateStandardSkillArgs, 'context'>) =>
createStandardSkillFlatMetadata({
...args,
context: {
skillName: 'view-filters-and-sorts',
name: 'view-filters-and-sorts',
label: 'View Filters & Sorts',
description:
'Adding filters and sorts to views to focus on relevant records based on user needs',
icon: 'IconFilter',
content: `# View Filters & Sorts Skill
You help users add filters and sorts to their views so they see the most relevant records.
## Tools
- get_views - List existing views to find the one to modify
- get_view_query_parameters - Check existing filters and sorts on a view
- list_object_metadata_items - Discover fields and their types to build valid filters
- create_view_filter / create_many_view_filters - Add filters to a view
- create_view_sort / create_many_view_sorts - Add sorts to a view
- navigate_app - Navigate to the view to show results
## Filter Operators by Field Type
| Field Type | Available Operators |
|---|---|
| TEXT, EMAILS, FULL_NAME, ADDRESS, LINKS, PHONES | CONTAINS, DOES_NOT_CONTAIN, IS_EMPTY, IS_NOT_EMPTY |
| NUMBER, NUMERIC | IS, IS_NOT, GREATER_THAN_OR_EQUAL, LESS_THAN_OR_EQUAL, IS_EMPTY, IS_NOT_EMPTY |
| CURRENCY | GREATER_THAN_OR_EQUAL, LESS_THAN_OR_EQUAL, IS_EMPTY, IS_NOT_EMPTY |
| DATE, DATE_TIME | IS, IS_RELATIVE, IS_IN_PAST, IS_IN_FUTURE, IS_TODAY, IS_BEFORE, IS_AFTER, IS_EMPTY, IS_NOT_EMPTY |
| SELECT | IS, IS_NOT, IS_EMPTY, IS_NOT_EMPTY |
| MULTI_SELECT, ARRAY | CONTAINS, DOES_NOT_CONTAIN, IS_EMPTY, IS_NOT_EMPTY |
| RELATION | IS, IS_NOT, IS_EMPTY, IS_NOT_EMPTY |
| BOOLEAN | IS |
## Sort Directions
- ASC: Ascending (A→Z, 0→9, oldest→newest)
- DESC: Descending (Z→A, 9→0, newest→oldest)
## Filter Groups (AND/OR/NOT)
Filters can be grouped with logical operators:
- **AND**: All filters must match (default)
- **OR**: At least one filter must match
- **NOT**: Negate the group
- Groups can be nested for complex conditions like: name CONTAINS "tech" AND (revenue > 1M OR employees > 100)
## Workflow
1. **Identify the view**: If the user didn't specify a view, ask which view they want to filter/sort. Use get_views to list available views and present them.
2. **Understand the need**: If the user hasn't described what they want to see, ask them. Give guidance with examples:
- "What records do you want to focus on? For example:"
- "Show only high-value opportunities (amount > $50K)"
- "Show companies in a specific city or industry"
- "Show tasks due this week, sorted by priority"
- "Show people from a specific company"
- "Show recent records created in the last 30 days"
3. **Inspect the view**: Use get_view_query_parameters to see existing filters/sorts and list_object_metadata_items to discover available fields.
4. **Build filters**: Based on the user's need, determine:
- Which field(s) to filter on
- Which operator is valid for that field type (see table above)
- What value to filter by
- Whether to use AND or OR grouping for multiple filters
5. **Build sorts**: Determine:
- Which field to sort by (most relevant to the user's goal)
- Direction: ASC or DESC
- Multiple sorts can be added (primary, secondary, etc.)
6. **Apply and navigate**: Create the filters/sorts on the view and navigate to it.
## Common Filter Patterns
### By Time
- Recent records: DATE_TIME field + IS_AFTER + a date value
- Upcoming deadlines: DATE field + IS_IN_FUTURE
- Overdue tasks: DATE field + IS_IN_PAST + status IS_NOT "DONE"
- This week/month: DATE field + IS_RELATIVE
### By Status/Stage
- Open opportunities: stage IS "IN_PROGRESS" or IS_NOT "WON"/"LOST"
- Active tasks: status IS_NOT "DONE"
### By Relationship
- Records linked to a company: company relation IS [specific company]
- Unassigned tasks: assignee IS_EMPTY
- Orphaned records: relation field IS_EMPTY
### By Value
- High-value deals: amount GREATER_THAN_OR_EQUAL threshold
- Large companies: employees GREATER_THAN_OR_EQUAL threshold
## Common Sort Patterns
- Pipeline view: Sort by amount DESC (biggest deals first)
- Task management: Sort by dueAt ASC (earliest due first)
- Recent activity: Sort by updatedAt DESC or createdAt DESC
- Alphabetical: Sort by name ASC
## Composite Fields
Some fields have sub-fields that can be filtered:
- CURRENCY: Use subFieldName "amountMicros" for the numeric value
- ADDRESS: Use subFieldName like "addressCity", "addressCountry"
- FULL_NAME: Use subFieldName like "firstName", "lastName"
- EMAILS: Use the primary email
- LINKS: Use the primary link URL
## Approach
- Always check field types before suggesting operators — using an invalid operator for a field type will fail
- When the user says "show me X", translate that into the appropriate filter logic
- Suggest sorts that complement the filters (e.g., if filtering overdue tasks, sort by dueAt ASC)
- Explain what the filters do so users understand the results
- If complex filtering is needed (AND + OR), explain the logic clearly`,
isCustom: false,
},
}),
'custom-objects-cleanup': (args: Omit<CreateStandardSkillArgs, 'context'>) =>
createStandardSkillFlatMetadata({
...args,
context: {
skillName: 'custom-objects-cleanup',
name: 'custom-objects-cleanup',
label: 'Custom Objects Cleanup',
description:
'Archiving custom objects from a workspace (e.g. dev seed objects like pets, rockets)',
icon: 'IconArchive',
content: `# Custom Objects Cleanup Skill
You help users archive custom objects from their workspace, such as objects created by the dev seed (pets, rockets, survey results, etc.) or any other custom objects.
## Tools
- list_object_metadata_items - List all objects in the workspace to identify custom ones
- update_many_object_metadata - Archive custom objects by setting isActive to false
## Workflow
1. **List all objects**: Use list_object_metadata_items to get the full list of objects in the workspace.
2. **Identify custom objects**: Filter the results to find objects where isCustom is true. These are the objects that were created by users or by the dev seed, as opposed to standard built-in objects (Company, Person, Opportunity, Task, Note, etc.).
3. **Present findings**: Tell the user which custom objects were found. If none are found, inform the user that the workspace has no custom objects.
4. **Confirm before archiving**: List the custom objects you found and ask the user to confirm which ones they want to archive. Present them clearly with their name, label, and description.
5. **Archive confirmed objects**: Use update_many_object_metadata to set isActive to false on all confirmed objects in a single batch call.
6. **Report results**: After archiving is complete, summarize what was archived.
## Important Notes
- Only objects with isCustom = true can be archived. Standard objects cannot be archived through this skill.
- Archiving an object hides it from the workspace but does not delete its fields, relations, or records.
- When called directly by a user, confirm before archiving. When called by another skill (e.g. workspace-demo-seeding), proceed without confirmation.
## Approach
- Be clear about what will be archived and that it is reversible
- If an object has relations to other objects, mention this before archiving
- Archive all confirmed objects in a single batch call using update_many_object_metadata`,
isCustom: false,
},
}),
pptx: (args: Omit<CreateStandardSkillArgs, 'context'>) =>
createStandardSkillFlatMetadata({
...args,