Bonapara/twenty codex plugin (#20857)

@martmull v2.0 ;)

---------

Co-authored-by: martmull <martmull@hotmail.fr>
Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
This commit is contained in:
Thomas des Francs
2026-06-02 16:39:14 +02:00
committed by GitHub
parent cb744b2eeb
commit 1642be86f5
52 changed files with 5151 additions and 0 deletions
@@ -0,0 +1,114 @@
# App Structure
Use this reference when creating or modifying files inside an existing Twenty app.
Use `../manage-app/cli-and-sync.md` for CLI command behavior, remotes, authentication, sync troubleshooting, build, deploy, logs, and CI/CD.
## App Checks
Before changing app entities, confirm the current directory is a Twenty app:
```bash
test -f package.json
test -f src/application-config.ts
```
If this fails, do not edit from the wrong folder. Check the current path and nearby app roots:
```bash
pwd
find . -maxdepth 3 -name package.json -o -path '*/src/application-config.ts'
```
Move to the matching app root before continuing. If no app exists, use `create-app`. If the folder exists but tooling, dependencies, remotes, authentication, sync, or builds are broken, use `manage-app` before changing app entities.
Inspect the app shape:
```bash
sed -n '1,220p' package.json
sed -n '1,220p' src/application-config.ts
find src -maxdepth 3 -type f | sort
find public -maxdepth 2 -type f | sort
```
## Entity Creation
Prefer the app CLI for new entities when interactive prompts are acceptable:
```bash
yarn twenty dev:add
```
For non-interactive agent work, direct file creation is often better. Use generated CLI templates, local SDK typings, or existing app files as the source of truth for imports and config shape.
Use official Twenty docs or local SDK source when exact imports, entity fields, or configuration shapes matter.
## Source Layout
Add these alongside the scaffolded `src/{fields,objects,logic-functions,front-components,page-layouts,navigation-menu-items,constants}/` as needed:
- `src/utils/` — pure helpers as `<name>.util.ts`, kept flat.
- `src/types/` — one PascalCase type per file.
- `src/<service>-client/` — wrappers around external SDKs or HTTP clients. One folder per service, matching Twenty's `*-client` convention (`redis-client/`, `sdk-client/`, ...).
- Tests as `*.spec.ts` in sibling `__tests__/` folders next to the code they cover.
Filenames are kebab-case; conventional suffixes are `.util.ts`, `.dto.ts`, `.service.ts`, `.spec.ts` — matching the Twenty backend.
### One Export Per File
Every helper, type, and client file exports exactly one thing. The rule counts exports, not declarations — there are no exceptions for `utils/`, `types/`, or `<service>-client/`.
- Never put multiple function exports in the same file. One function per file, each with its own sibling spec.
- A file must never export more than one thing. A local (non-exported) type used only by the util may live in the same file, but the moment a type is exported or reused elsewhere it moves to `src/types/<name>.ts` while the util stays in `src/utils/<name>.util.ts`.
```ts
// ❌ Bad — src/utils/parse-company.util.ts exports a type and a util
export type ParsedCompany = { id: string; name: string };
export const parseCompany = (raw: RawCompany): ParsedCompany => { /* ... */ };
// ✅ Good — src/utils/parse-company.util.ts (one export; the type is local)
type ParsedCompany = { id: string; name: string };
export const parseCompany = (raw: RawCompany): ParsedCompany => { /* ... */ };
// ✅ Good — when the type is shared, split it
// src/types/parsed-company.ts (one PascalCase type)
export type ParsedCompany = { id: string; name: string };
// src/utils/parse-company.util.ts (one util)
import { type ParsedCompany } from 'src/types/parsed-company';
export const parseCompany = (raw: RawCompany): ParsedCompany => { /* ... */ };
```
Entity files are consistent with this rule: front components, logic functions, and post-install hooks use a single `export default define...()`, which is one export.
Files inside `src/types/` use plain `<name>.ts` (one PascalCase type per file) — no `.type.ts` suffix. Files inside `src/<service>-client/` also use plain `<name>.ts`; the folder name carries the suffix, so do not repeat it on the file.
When field definitions differ only by `objectUniversalIdentifier` and label, replace them with a factory in `src/fields/field-factories.ts`.
Read secrets through the application-config helper, not raw `process.env`.
## Boundaries
- Do not scaffold a new app from this workflow. Use `create-app` first when the app does not exist.
- Do not guess generated entity shapes when the CLI or docs can provide them.
- Keep app changes scoped to the requested feature and its required registrations.
## Validation Checklist
Once all edits for the change are complete, run lint and typecheck once at the end (not after each individual edit), then sync the app to verify the definitions are valid:
```bash
yarn twenty dev:typecheck
yarn lint
yarn twenty dev --once
```
`yarn twenty dev:typecheck` checks generated app types and `yarn lint` checks local lint rules. `yarn twenty dev --once` then builds the app and pushes entity definitions to the active remote; if any definition is invalid, the sync reports the error. Run all three a single time once every edit is done, not repeatedly after each step.
When the user explicitly asks to run tests, follow `tests.md`: unit tests may use the package's unit-test script, and the full suite must run with `TWENTY_API_URL=http://localhost:2021` against the isolated test instance.
Switch to `manage-app` and use `../manage-app/cli-and-sync.md` for sync or remote troubleshooting.
Tests are not part of sync validation but should cover `src/utils/` helpers and post-install hook idempotency. See `tests.md`.
@@ -0,0 +1,108 @@
# Data Model
Use this reference for Twenty app objects, fields, relations, roles, and permissions.
## Objects And Fields
Objects and fields define the records users will create, view, search, and automate. Model the user's workflow first, then add the smallest set of fields that makes the workflow usable.
When adding or modifying data model entities:
- Prefer generated app entity patterns from `yarn twenty dev:add`.
- Use clear object and field names that map to user-facing language.
- Add relations only when users need to navigate or report across records.
- Avoid duplicating data that already exists on core Twenty objects.
- Keep field types specific enough to support filtering, views, and automation.
### Usable Object Pattern
When the user asks for a new object and does not explicitly restrict the request to schema only, make the object usable in the app by pairing data model work with the minimum layout/navigation surfaces:
- Object file in `src/objects/<name>.ts`
- Table view in `src/views/all-<plural>.ts`
- Object navigation item in `src/navigation-menu-items/<name>.ts`
- Record page layout in `src/page-layouts/<name>-record-page-layout.ts`
- Fields-widget view for the record page in `src/views/<name>-record-page-fields.ts`
Use `layout.md` for view, navigation, and page layout details.
Objects support `icon`, but object color is not defined on `defineObject()`. Put color on the object navigation item:
```ts
import {
defineNavigationMenuItem,
NavigationMenuItemType,
} from 'twenty-sdk/define';
export default defineNavigationMenuItem({
universalIdentifier: '<uuid>',
name: '<name>',
icon: '<IconName>',
color: '<color>',
position: 0,
type: NavigationMenuItemType.OBJECT,
targetObjectUniversalIdentifier: '<object-uuid>',
});
```
### Minimal Object Example
```ts
import { defineObject, FieldType } from 'twenty-sdk/define';
export const OBJECT_UNIVERSAL_IDENTIFIER = '<uuid>';
export const NAME_FIELD_UNIVERSAL_IDENTIFIER = '<uuid>';
export default defineObject({
universalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
nameSingular: '<name>',
namePlural: '<names>',
labelSingular: '<Name>',
labelPlural: '<Names>',
icon: '<IconName>',
labelIdentifierFieldMetadataUniversalIdentifier:
NAME_FIELD_UNIVERSAL_IDENTIFIER,
fields: [
{
universalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.TEXT,
name: 'name',
label: 'Name',
icon: 'IconAbc',
},
],
});
```
### Select Fields
Select and multi-select option `value` strings must be uppercase snake case:
```ts
{
type: FieldType.SELECT,
name: 'status',
label: 'Status',
defaultValue: "'PLANNED'",
options: [
{ position: 0, label: 'Planned', value: 'PLANNED', color: 'sky' },
{ position: 1, label: 'In build', value: 'IN_BUILD', color: 'orange' },
],
}
```
For select fields, default values must be quoted string expressions like `"'PLANNED'"`, not plain strings like `'planned'`.
## Roles And Permissions
Roles should match operational responsibility, not implementation convenience.
When adding permissions:
- Grant the smallest useful scope.
- Keep sensitive objects and fields out of broad roles.
- Check whether the app introduces side effects through logic functions before granting write access.
## Verification
After data model changes, run the app and verify the objects, fields, and roles appear where the user will manage or use them.
@@ -0,0 +1,121 @@
# Front Components
Use this reference for Twenty app front component source files, registration, data access, Twenty UI imports, runtime imports, and browser verification.
Use `layout.md` for placing a front component in a page layout. Use `standalone-pages.md` for full-page custom UI rendered through a front component widget. Use `../design/front-component-ui.md` for visual design, spacing, states, and polish. Use `app-structure.md` for the develop-app validation checklist and `../manage-app/cli-and-sync.md` for command behavior or troubleshooting.
## Source And Registration
By convention, keep front components under `src/front-components/`, often as `<name>.front-component.tsx`.
Register the component with `defineFrontComponent`:
```tsx
import { defineFrontComponent } from 'twenty-sdk/define';
const MyFrontComponent = () => {
return <div />;
};
export default defineFrontComponent({
universalIdentifier: '<front-component-uuid>',
name: '<front-component-name>',
description: '<short description>',
component: MyFrontComponent,
});
```
Do not import or call `react-dom/client` in the component source. The SDK renderer mounts front components.
## Runtime Imports
Keep imports narrow:
- Use `twenty-sdk/front-component` for front component hooks and host APIs.
- Use `twenty-client-sdk/core` or `twenty-client-sdk/metadata` for data access.
- Use `twenty-sdk/ui` for Twenty UI components, icons, and theme tokens before adding external UI libraries.
- Do not import from `twenty-ui` directly. Use the SDK re-export so build aliases and runtime packaging stay aligned.
The front component renderer provides a Twenty `ThemeProvider` around the remote root. For isolated examples or local story-style verification, wrapping the component in `ThemeProvider` from `twenty-sdk/ui` is also acceptable.
The canonical Twenty UI front component example is `packages/twenty-front-component-renderer/src/__stories__/example-sources/twenty-ui-example.front-component.tsx`. It imports `Button`, `Chip`, `H2Title`, `Status`, `Tag`, and `ThemeProvider` from `twenty-sdk/ui`.
When using `themeCssVariables`, compute styles inside the component body or a function called by the component. The SDK mocks `twenty-sdk/ui` during manifest extraction, so module-level constants that dereference `themeCssVariables` can fail before the component renders.
Prefer Twenty UI icons from `twenty-sdk/ui` when one exists. Use inline SVG only for app-specific marks or icons that are not available through the SDK export.
## Record Context And Data
For record-page components, read selection from front component context:
- Use `useSelectedRecordIds()` for single, bulk, or empty selection; derive one id only when the array length is 1.
Use the generated or core Twenty client for reads and writes. Keep loading, empty, error, disabled, and saving states explicit so runtime failures are visible and recoverable.
## Headless Actions And DRY Helpers
Headless front components should be thin action shells. The component file should mostly read SDK hooks, return the `Command` helper, and delegate reusable behavior to helpers.
Common headless action flow:
1. Read selected record IDs.
2. Load the selected records.
3. Build a payload for one logic-function call.
4. Execute the logic function.
5. Parse the result summary.
6. Show a snackbar.
7. Let `Command` unmount the component.
Do not duplicate this orchestration across sibling front components. When two components share command execution flow, extract it before copying:
- Pure helpers go in `src/utils/<name>.util.ts`.
- Front-component runtime helpers go in `src/front-components/utils/<name>.util.ts`.
- Payload builders, result parsers, selected-record validators, and summary formatters should be small testable functions with sibling specs.
Each extracted helper and type lives in its own file: one export per file. A local, non-exported type may stay with the util, but an exported type moves to its own file. See `app-structure.md`.
Prefer configuration-driven helpers when creating parallel actions for people, companies, tasks, or other objects. Each front component should provide only object-specific configuration, such as the front component universal identifier, logic function universal identifier, record query, payload builder, labels, and snackbar copy.
## Bulk Logic Function Calls
When a front component triggers a logic function for selected records, always prefer a bulk payload shape unless the user explicitly says the logic function is only for one record. The front component should call the logic function once with all selected records, not loop and execute the same logic function once per record.
Default payload shape:
```ts
{
records: Array<{
id: string;
// object-specific fields used by the logic function
}>;
}
```
Inside `records`, prefer `id` for the Twenty record ID because the array name already establishes record context. Do not add flat `recordId` payloads unless the user explicitly asks for a single-record action.
Logic functions that return per-record outcomes should mirror the same naming:
```ts
{
ok: boolean;
enrichedCount: number;
noMatchCount: number;
failedCount: number;
results: Array<{
id: string;
status: 'ENRICHED' | 'NO_MATCH' | 'FAILED';
error?: string;
}>;
}
```
If an existing logic function accepts only a flat record ID, prefer upgrading it to accept `records` and remove the old flat input unless the user explicitly asks to preserve it.
## Runtime Verification
A clean typecheck and sync is not runtime verification. After the standard validation in `app-structure.md`, open the relevant Twenty surface and confirm:
- The widget mounts without a `FrontComponent error` in the widget body or toast.
- The component renders loading, empty, and error states correctly.
- The main user action works against a real record.
- The page still renders after a hard refresh.
@@ -0,0 +1,50 @@
# Layout
Use this reference for Twenty app views, navigation, page layouts, page layout tabs, and front component registration. Use `standalone-pages.md` when a page layout is meant to host a full-page custom UI.
## Views And Navigation
Views and navigation define the first-run experience. Add only the surfaces users need to understand and operate the app.
When changing views:
- Put the most common workflow first.
- Use concise names that match the data model.
- Keep list, board, and detail surfaces consistent with existing Twenty patterns.
- Avoid creating navigation entries for low-frequency admin tasks unless users need repeated access.
## Page Layouts
Page layouts should make the record's current state and next action easy to scan.
When adding layouts or tabs:
- Group related fields and components.
- Keep important record status visible without scrolling when possible.
- Place front components where they support the surrounding record context.
- Include empty and loading behavior for front components shown on record pages.
## Front Component Widgets
When adding an app-defined front component to a record page layout, use the component's universal identifier in the widget configuration:
```ts
{
universalIdentifier: '<widget-uuid>',
title: '<Widget title>',
type: 'FRONT_COMPONENT',
objectUniversalIdentifier: '<object-uuid>',
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier: '<front-component-uuid>',
},
}
```
Use `frontComponentUniversalIdentifier` for app-defined front components. A `frontComponentId` is not the same value and will not link the widget to the app component correctly.
## Verification
Run the app and inspect the user path from navigation to view to record detail. The route should be discoverable without relying on implementation knowledge.
For front component implementation and runtime verification after placement, use `front-components.md`.
@@ -0,0 +1,103 @@
# Logic
Use this reference for Twenty app logic functions, skills, agents, and connection providers.
## Logic Functions
A logic function file should contain trigger registration, input validation, the call out to the work, and the writes back — nothing else. Everything else lives next to it (see `app-structure.md`):
- External API calls → `src/<service>-client/<name>.ts`.
- Response parsing and mapping → `src/utils/<name>.util.ts`.
- Response and DTO types → `src/types/<name>.ts`.
- Shared structure across object types → one mapper factory parameterized by object kind, not parallel functions.
Kebab-case filenames, one export per file. Response/DTO types go in `src/types/<name>.ts` and parsing/mapping helpers in `src/utils/<name>.util.ts` — never export both a type and a util from the same file (a local, non-exported type may stay with the util), and never put multiple function exports in one file. See `app-structure.md`.
Other rules:
- Validate required fields before writes or remote calls.
- Prefer bulk inputs for record actions. If a logic function can be triggered from selected records, the canonical input should be `records: Array<{ id: string; ...fields }>` unless the user explicitly says the function is only for one record.
- Use `id` inside a `records` array for the Twenty record ID. Do not add `recordId`, object-specific IDs such as `companyId`, or flat single-record payloads unless the user explicitly requests a single-record contract.
- Return a bulk summary with per-record results for multi-record actions, including counts for success, no match, and failed records.
- Prefer idempotent behavior for jobs and repeated invocations.
- Read secrets through the application-config helper, not raw `process.env`.
- Do not hide customer-impacting side effects behind UI-only actions.
Soft cap: a `*.logic-function.ts` or `*.post-install.ts` file over 200 lines is a refactor signal.
## Bulk Record Actions
Bulk is the default logic-function contract for actions that may run from front component selection. The front component should gather selected records and invoke the function once. Do not make the front component loop over selected records and execute the same logic function repeatedly unless there is an explicit single-record requirement.
Preferred input:
```ts
type BulkInput<TRecord extends { id: string }> = {
records: TRecord[];
};
```
Preferred output:
```ts
type BulkResult = {
ok: boolean;
enrichedCount: number;
noMatchCount: number;
failedCount: number;
results: Array<{
id: string;
status: 'ENRICHED' | 'NO_MATCH' | 'FAILED';
pdlId?: string;
error?: string;
}>;
};
```
For existing single-record functions that are being upgraded to selected-record actions, replace the old flat input with `records: Array<{ id: string; ...fields }>` unless the user explicitly asks for backward compatibility.
Extract and test:
- input normalization for the canonical bulk shape;
- per-record validation;
- external API payload mapping;
- per-record result mapping;
- summary count aggregation;
- error message normalization.
## Skills And Agents
Skills and agents should describe when they apply, what context they need, and what output is expected.
When adding AI behavior:
- Make trigger rules concrete.
- Keep instructions grounded in available app data and tools.
- State when the agent should ask for missing workspace or record context.
- Avoid exposing raw IDs, timestamps, or nested API output to end users when a readable answer is possible.
## Connection Providers
For third-party connections:
- Keep secrets out of source and public assets.
- Document required OAuth or API setup in the app README or listing.
- Verify failure states for expired or missing credentials.
## Post-Install Hooks
Use `definePostInstallLogicFunction` for records that must exist on install — default workflows, views, roles, or seeded reference data. Do not implement this as runtime first-run code.
Post-install hook files live alongside other logic functions (typically `src/logic-functions/<name>.post-install.ts`). Kebab-case filename, one export per file.
Hooks must be idempotent: find by stable identifier before creating, update if it exists, never duplicate. Treat a not-found from a single-record query as "needs create."
Do not write fields Twenty computes elsewhere (workflow `statuses` is computed from version status — see `workflows.md`).
Dev sync skips install hooks. Invoke locally:
```bash
yarn twenty dev:function:exec
```
Run again after rebuilding to verify idempotency.
@@ -0,0 +1,458 @@
# Standalone Pages
Use this reference when a Twenty app needs a full-page custom UI: an operational console, map, canvas, planner, status wall, or other page-sized tool.
For general page layout and navigation entities, use `layout.md`. For front component source, runtime imports, hooks, data access, and browser verification, use `front-components.md`. For visual polish and Twenty UI component choices, use `../design/front-component-ui.md`. For CLI and deployment command details, use `../manage-app/cli-and-sync.md`.
This reference owns only the standalone-page assembly pattern and full-page behavior. It repeats small code fragments where they are needed to show how the pieces connect, but leaves general API behavior to the linked references.
## Mental Model
A standalone page is not a raw page body component. In the current local app pattern, custom page content should be rendered through a `FRONT_COMPONENT` widget inside a `STANDALONE_PAGE` page layout. There does not appear to be a separate public "page body component" API for app-defined standalone pages.
The current model is:
1. Define stable universal identifiers.
2. Register the page experience with `defineFrontComponent`.
3. Place that front component in a `definePageLayout` with `type: 'STANDALONE_PAGE'`.
4. Surface the page with `defineNavigationMenuItem` using `NavigationMenuItemType.PAGE_LAYOUT`.
5. Sync or install the app.
6. Twenty resolves the sidebar item to the `/page/:pageLayoutId` route and renders the front component widget inside the page layout.
Use these surfaces for different jobs:
| Surface | Use it for | Primary app entity |
| --- | --- | --- |
| Standalone page | A full workspace page that is not tied to one record | `STANDALONE_PAGE` + `PAGE_LAYOUT` navigation item + `FRONT_COMPONENT` widget |
| Record page layout | Tabs and widgets for one object record | `RECORD_PAGE` page layout or page layout tab |
| Dashboard | Metric and report composition from built-in widgets | `DASHBOARD` page layout |
| Command or side panel | Short actions, focused forms, one selected record, or background commands | `defineFrontComponent` plus command menu item |
## Removing The Scaffolded Placeholder
Every freshly scaffolded app contains three placeholder files that wire a "Welcome" sidebar item to a generic landing page:
- `src/front-components/main-page.tsx`
- `src/page-layouts/main-page.page-layout.ts`
- `src/navigation-menu-items/main-page.navigation-menu-item.ts`
If the app has no user-facing page — for example, it only extends standard objects, declares logic functions, or seeds workflows — delete all three files before the first deploy. Leaving them in ships a dead sidebar item.
## Quickstart
Use this minimal file set:
- `src/constants/universal-identifiers.ts`
- `src/front-components/<name>.front-component.tsx`
- `src/page-layouts/<name>.page-layout.ts`
- `src/navigation-menu-items/<name>.navigation-menu-item.ts`
Start with a tiny component that proves the route works before building the full page.
```ts src/constants/universal-identifiers.ts
export const MISSION_CONTROL_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER =
'a0a1c4f0-f23a-4c59-93c5-92146d64b110';
export const MISSION_CONTROL_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER =
'63d4970d-6c54-49c8-9c15-52d7cb00fb7a';
```
```tsx src/front-components/mission-control.front-component.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import {
MISSION_CONTROL_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
} from '../constants/universal-identifiers';
const MissionControl = () => {
return (
<main
style={{
boxSizing: 'border-box',
display: 'grid',
minHeight: '100%',
padding: 24,
placeItems: 'center',
width: '100%',
}}
>
<h1 style={{ margin: 0 }}>Mission Control</h1>
</main>
);
};
export default defineFrontComponent({
universalIdentifier: MISSION_CONTROL_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
name: 'mission-control',
description: 'Standalone mission control page.',
component: MissionControl,
});
```
```ts src/page-layouts/mission-control.page-layout.ts
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
import {
MISSION_CONTROL_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
MISSION_CONTROL_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
} from '../constants/universal-identifiers';
export default definePageLayout({
universalIdentifier: MISSION_CONTROL_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
name: 'Mission Control',
type: 'STANDALONE_PAGE',
tabs: [
{
universalIdentifier: 'e6963ad3-e5fa-41c7-83e1-4fc2ca5de9a8',
title: 'Mission Control',
position: 0,
icon: 'IconRocket',
layoutMode: PageLayoutTabLayoutMode.CANVAS,
widgets: [
{
universalIdentifier: '18ce05bb-ee3c-4332-80a7-f8fb84f7f70a',
title: 'Mission Control',
type: 'FRONT_COMPONENT',
gridPosition: { row: 0, column: 0, rowSpan: 12, columnSpan: 12 },
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier:
MISSION_CONTROL_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
},
],
},
],
});
```
```ts src/navigation-menu-items/mission-control.navigation-menu-item.ts
import {
defineNavigationMenuItem,
NavigationMenuItemType,
} from 'twenty-sdk/define';
import {
MISSION_CONTROL_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
} from '../constants/universal-identifiers';
export default defineNavigationMenuItem({
universalIdentifier: '61d44a63-16e8-4fbe-bccb-9c220d44fdb9',
name: 'Mission Control',
icon: 'IconRocket',
color: 'blue',
position: 50,
type: NavigationMenuItemType.PAGE_LAYOUT,
pageLayoutUniversalIdentifier:
MISSION_CONTROL_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
});
```
Use `PageLayoutTabLayoutMode.CANVAS` for the full-page renderer. Keep the 12 x 12 fill pattern as a grid fallback and editing hint; CANVAS renders the first widget as the page-sized surface.
After the tiny page renders, replace the component body with a full-screen structure:
```tsx
const MissionControl = () => {
return (
<main
style={{
boxSizing: 'border-box',
display: 'grid',
gap: 16,
gridTemplateRows: 'auto minmax(0, 1fr)',
height: '100%',
minHeight: '100%',
overflow: 'hidden',
padding: 16,
width: '100%',
}}
>
<header style={{ display: 'flex', justifyContent: 'space-between' }}>
<h1 style={{ margin: 0 }}>Mission Control</h1>
<button type="button">Refresh</button>
</header>
<section
style={{
display: 'grid',
gridTemplateColumns: '280px minmax(0, 1fr)',
minHeight: 0,
overflow: 'hidden',
}}
>
<aside style={{ minHeight: 0, overflow: 'auto' }}>Filters</aside>
<div style={{ minHeight: 0, overflow: 'auto' }}>Workspace data</div>
</section>
</main>
);
};
```
## API Reference
This section only calls out the fields that matter for standalone pages. Use `layout.md` and `front-components.md` for broader entity guidance.
`definePageLayout` owns the standalone route target:
- Use `type: 'STANDALONE_PAGE'`.
- Do not set `objectUniversalIdentifier`; standalone pages are not record scoped.
- Define at least one tab. Use one tab unless the page needs real top-level modes.
- Use `PageLayoutTabLayoutMode.CANVAS`; put the `FRONT_COMPONENT` first and keep a 12 x 12 `gridPosition` as fallback/editing intent.
- Use a `FRONT_COMPONENT` widget with `configurationType: 'FRONT_COMPONENT'` and `frontComponentUniversalIdentifier`.
`defineFrontComponent` owns the actual page experience:
- Register a visible component with `component`, `name`, `description`, and a stable `universalIdentifier`.
- Use `twenty-sdk/front-component` for runtime hooks, host navigation, snackbars, application variables, and side panel actions.
- Use `twenty-client-sdk/core` for workspace records when the page is data driven.
- Use `getPublicAssetUrl` from `twenty-sdk/define` for app-bundled images or static files.
`defineNavigationMenuItem` owns sidebar reachability:
- Use `type: NavigationMenuItemType.PAGE_LAYOUT`.
- Set `pageLayoutUniversalIdentifier` to the standalone page layout universal identifier.
- Set a clear `name`, `icon`, `color`, and `position`.
- Use `folderUniversalIdentifier` only when the page belongs under an existing app folder.
Public assets and non-secret application variables make standalone pages richer without hardcoding environment data:
```tsx
import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define';
import { getApplicationVariable } from 'twenty-sdk/front-component';
const logoUrl = getPublicAssetUrl('mission-control-logo.png');
const MissionBrand = () => {
const label = getApplicationVariable('MISSION_LABEL') ?? 'Mission Control';
return <img src={logoUrl} alt={label} />;
};
```
## Full-Page Layout Guidance
The front component only fills the page if every layer inside the widget has deterministic sizing. Start the root at `height: '100%'`, `minHeight: '100%'`, `width: '100%'`, and `boxSizing: 'border-box'`.
Use `minmax(0, 1fr)` and `minHeight: 0` for scrollable grid or flex children. Without these constraints, inner tables, maps, and canvases can force the page taller than the widget or collapse into a zero-height area.
Prefer one immersive tool surface over a dashboard made of many small cards. If the page is a mission tracker, map, editor, kanban, planner, or cockpit, make the front component own the composition and use internal panels only where they support the workflow.
Assume the available area changes with Twenty chrome, the left sidebar, side panel state, tab list, and smaller screens. Use responsive CSS inside the front component:
- Collapse side filters above or below the main surface on narrow widths.
- Keep map, canvas, table, and timeline containers at `minHeight: 0`.
- Make only intentional regions scroll.
- Keep loading, empty, and error states inside the same sized root so the page never goes blank while data changes.
Avoid hidden overflow traps. `overflow: 'hidden'` is useful for map and canvas roots, but pair it with explicit scroll containers for lists and diagnostics.
## Data And Interactivity
Fetch live workspace records from the front component when the page reflects workspace state:
Use `front-components.md` for general client/runtime rules. In standalone pages, the key additions are visible full-page loading, empty, and error states, plus navigation from the standalone surface back into Twenty records.
```tsx
import { useEffect, useState } from 'react';
import { CoreApiClient, type CoreSchema } from 'twenty-client-sdk/core';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
AppPath,
enqueueSnackbar,
navigate,
} from 'twenty-sdk/front-component';
type CompanySummary = Pick<CoreSchema.Company, 'id' | 'name'>;
const CompaniesStandalonePage = () => {
const [companies, setCompanies] = useState<CompanySummary[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
const loadCompanies = async () => {
try {
setLoading(true);
setError(null);
const client = new CoreApiClient();
const result = await client.query({
companies: {
edges: {
node: {
id: true,
name: true,
},
},
},
});
if (!cancelled) {
setCompanies(result.companies.edges.map((edge) => edge.node));
}
} catch (err) {
const message =
err instanceof Error ? err.message : 'Failed to load companies';
if (!cancelled) {
setError(message);
enqueueSnackbar({ message, variant: 'error' });
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
};
loadCompanies();
return () => {
cancelled = true;
};
}, []);
if (loading) return <div style={{ padding: 24 }}>Loading companies...</div>;
if (error) return <div style={{ padding: 24 }}>{error}</div>;
if (companies.length === 0) {
return <div style={{ padding: 24 }}>No companies yet.</div>;
}
return (
<div style={{ display: 'grid', gap: 8, padding: 24 }}>
{companies.map((company) => (
<button
key={company.id}
type="button"
onClick={() =>
navigate(AppPath.RecordShowPage, {
objectNameSingular: 'company',
objectRecordId: company.id,
})
}
>
{company.name}
</button>
))}
</div>
);
};
export default defineFrontComponent({
universalIdentifier: '5aa8c51f-72a4-4929-a6ab-2d4ea9d2a6df',
name: 'companies-standalone-page',
description: 'Lists companies and opens related records.',
component: CompaniesStandalonePage,
});
```
Always model these states:
- Loading: show progress in the same page shell that the loaded view uses.
- Empty: explain what is missing and offer the next action if one exists.
- Error: show the message and keep a retry path visible.
- Auth refresh: token failures can happen during development or long-lived sessions; surface the failure instead of returning `null`.
- Demo data: label fake data clearly and keep the switch to live data obvious.
Use `navigate` for full-page routes and `openSidePanelPage` for focused side-panel workflows. Prefer navigating to the record route when the user is leaving the standalone experience to inspect a real record.
## Debugging
For a black screen, check the simplest causes first:
- Component runtime exception: open the browser console and look for a front component error, React error, or Remote DOM unsupported operation.
- Missing data fallback: temporarily replace the component with the tiny "Mission Control" component from this reference.
- Token or fetch failure: log the caught error, confirm `CoreApiClient` generation, and show an error state.
- Public asset failure: verify `getPublicAssetUrl(...)` output in the network tab and render without the asset.
- CSS layer covering content: remove absolute overlays, `zIndex`, and full-screen backgrounds until text is visible.
- Zero-height container: add visible borders and confirm `height: '100%'`, `minHeight: '100%'`, `minHeight: 0`, and the 12 x 12 widget grid position.
- Unsupported browser or Remote DOM behavior: remove unusual DOM APIs, portals, global document access, and third-party components until the minimal UI renders.
- Stale deployed app version: confirm the installed app version is the one you just synced or deployed.
Console checks:
```tsx
console.info('Standalone page mounted');
console.info('Companies loaded', companies.length);
```
Installed-app checks:
- Confirm the app appears in Twenty settings or the application developer view.
- Confirm the sidebar item appears with the expected icon and label.
- Confirm clicking the sidebar item opens `/page/:pageLayoutId`.
- Confirm the page still renders after a hard refresh.
Known-good component test:
```tsx
const KnownGoodStandalonePage = () => (
<main style={{ minHeight: '100%', padding: 24 }}>
<h1>Standalone page runtime is working</h1>
</main>
);
```
If this renders but the full page does not, the issue is inside the full component. If this does not render, inspect the layout, navigation, sync, installation, and front component registration.
## Deployment And Verification
Use local dev sync while iterating and one-shot sync for bounded verification. Use `../manage-app/cli-and-sync.md` for exact command behavior, remote setup, verbose troubleshooting, deploys, and logs.
```bash
yarn twenty dev --once
```
Install and update flow:
- During development, sync against the active remote and confirm the app appears as installed in the target workspace.
- For an already installed app, sync or deploy the updated version, then reopen the sidebar route and hard refresh the page.
- For packaged deploys, bump `package.json` before publishing an update to a workspace that already has the app installed.
Versioning expectations:
- Dev sync updates the active remote during development.
- Deploying a packaged update requires a strictly higher `package.json` version than the installed version.
- Users may need to update or reinstall the app depending on how the target workspace receives application updates.
Acceptance checks:
- Sidebar item appears in the expected section or folder.
- Sidebar item opens the standalone page route.
- The front component renders visible content.
- The component fills the widget/page area at desktop size.
- Data loads from live workspace records or a clearly labeled demo source.
- Loading, empty, and error fallbacks are visible and nonblank.
- Record navigation or side panel interactions work.
- Desktop and smaller-screen screenshots are nonblank and do not show overlapping controls.
## Examples
Minimal standalone page:
- One front component.
- One `STANDALONE_PAGE` page layout.
- One `PAGE_LAYOUT` navigation item.
- One 12 x 12 `FRONT_COMPONENT` widget.
Full-screen operational page:
- Root grid with header and `minmax(0, 1fr)` body.
- Left filter panel, central work surface, optional right inspector.
- Loading, empty, error, and retry UI inside the same shell.
Data-driven page that opens related records:
- Fetch records with `CoreApiClient`.
- Render a list, table, timeline, or map.
- Use `navigate(AppPath.RecordShowPage, { objectNameSingular, objectRecordId })` for record drill-in.
Immersive canvas or map-style page, such as a Space X Mission Tracking page:
- Keep the map/canvas as the main full-height surface.
- Put filters, mission status, and selected mission details in internal panels.
- Use public assets for mission patches or map overlays.
- Avoid composing the page as dashboard cards unless the user is primarily comparing metrics.
@@ -0,0 +1,46 @@
# Tests
Tests use the `*.spec.ts` extension and live in sibling `__tests__/` folders next to the code they cover, matching Twenty backend conventions. Write them where `yarn twenty dev --once` does not validate correctness.
Always write a test file for every util or function. Whenever you create or modify a file in `src/utils/` (or any other testable function file), you MUST create or update its sibling `__tests__/<name>.spec.ts`. A util or function without a spec file is incomplete.
## File Organization
Write one spec file per util or function source file. Each testable source file gets a dedicated sibling `__tests__/<name>.spec.ts` whose name mirrors the source file. Do not combine multiple utils or functions into a single spec file.
```
src/utils/parse-foo.util.ts
src/utils/__tests__/parse-foo.util.spec.ts
src/utils/map-bar.util.ts
src/utils/__tests__/map-bar.util.spec.ts
```
## What To Test
- Parsers in `src/utils/` — every branch of foreign API shape handling, including missing fields and empty responses.
- Mappers in `src/utils/` — foreign-to-Twenty mapping, including the "no match" case.
- Front-component helpers in `src/front-components/utils/` — selected-record validation, payload builders, logic-function result parsing, summary aggregation, and snackbar message formatting.
- Bulk logic-function normalization — the canonical `records: Array<{ id: string; ...fields }>` shape, including empty arrays and records missing `id`.
- Post-install hooks — at minimum, idempotency: run twice, assert state is identical.
- Side effects (billing, external state) — assert they fire only on the conditions the code claims.
## What To Skip
- Entity definitions (`*.field.ts`, `*.object.ts`) — sync validates these.
- Front component rendering shells — verify in the browser. Extracted helper functions are not rendering shells and must be unit-tested.
## Running Tests
Integration tests build, deploy, install, and uninstall the app on whatever server `TWENTY_API_URL` points to. The scaffold defaults to the dev instance (`http://localhost:2020`), so running them there adds and then removes the app from the workspace you sync to with `yarn twenty dev --once`. Always run them against the isolated test instance instead — a separate container, database, and port (`2021`) that does not affect your running dev instance:
```bash
# Start the isolated test instance (once).
yarn twenty docker:start --test
# Run integration tests against it.
TWENTY_API_URL=http://localhost:2021 yarn test
```
The seeded default `TWENTY_API_KEY` works for both instances, so only the URL needs overriding. This mirrors CI, which spawns the same isolated instance via the `spawn-twenty-app-dev-test` action.
When the user asks you to run tests, do run them. First start or verify the isolated test instance, then run the test command with `TWENTY_API_URL=http://localhost:2021`. Do not run integration tests against `http://localhost:2020` unless the user explicitly asks to target the dev instance. If only unit tests are requested, use the package's unit-test script and no `TWENTY_API_URL` override is needed.
@@ -0,0 +1,47 @@
# Workflows
Use when the app needs to ship a manual workflow on install, or when a logic function should be invocable from the workflow builder.
See `logic.md` for post-install hooks. See `app-structure.md` for source layout.
## Mental Model
Twenty workflows are workspace records, not app entities — no `define*` primitive. Ship them via `definePostInstallLogicFunction` and the workspace API.
A workflow is a `Workflow` plus at least one `WorkflowVersion`. Creating a `Workflow` auto-creates its draft `v1`; never create a `WorkflowVersion` directly.
## Manual Record Trigger
- Trigger type: manual record selection.
- The selected record is the trigger payload — no wrapping `record` field.
- Reference fields with `{{trigger.<field>}}`. Do not use `{{trigger.record.<field>}}`.
## Lifecycle
Always: create → configure draft → activate. Never publish a draft directly.
1. Find or create the `Workflow` by stable name or slug. `createWorkflow` produces the draft `v1`.
2. Set the trigger on the draft via `updateWorkflowVersion`.
3. Add steps via the workflow-step mutations. `createWorkflowVersionStep` returns a `stepsDiff`, not the step — read the diff for the new step id, then call `updateWorkflowVersionStep` with the full payload.
4. Activate via `activateWorkflowVersion`.
Forbidden:
- Do not write `Workflow.statuses` — it is computed from the active version's status.
- Do not create `WorkflowVersion` rows directly.
## Idempotency
Find by deterministic name or slug before creating. Single-record queries throw on absence in some Twenty versions — treat not-found as "needs create."
## Permissions
Seeders need workflow settings permissions on the app role. Grant via `defineRole`. If a typed mutation is rejected from the app context, fix the role — falling back to raw GraphQL signals the wrong scope.
## Invoking Locally
```bash
yarn twenty dev:function:exec
```
`yarn twenty dev --once` skips install hooks. Run again after rebuilding to verify idempotency.