Skip to main content

Aventora CRM — complete guide

Single reference for aventora-crm (Twenty fork): deployment, upgrade, provisioning/SSO, industry presets, workspace color themes, Sales Cockpit, TIPS sync, and troubleshooting.

Audience: operators, integrators, client backend developers, sales ops.

Laravel custom integration (no plugin): CRM_LARAVEL_INTEGRATION.md — provisioning, SSO, embedded navigation, avatar.

Related platform docs: API_SECURITY_MODEL.md, platform-api-keys.md, CRM_OUTBOUND_EMAIL_TEMPLATES.md

Repo: aventora-crm — deployment assets in docker/, code customizations in packages/twenty-server/src/engine/core-modules/.


Table of contents

  1. Concepts
  2. Environment variables
  3. Deployment and upgrade
  4. Admin CLI commands
  5. Provisioning and SSO
  6. Workspaces, users, and industry presets
  7. Sales Cockpit
  8. Demo workspace provisioning
  9. TIPS Services sync
  10. Laravel plugin
  11. Contacts and workspace API keys
  12. Webhooks
  13. Troubleshooting
  14. Code map
  15. Changelog

Concepts

ConceptCRM meaningAPI surface
UserPerson who can log in to a workspacePOST /auth/provision/* on apex CRM_API_URL
Contactperson record in the People modulePOST /rest/people on workspace host with workspace API key

Provisioning and SSO use the apex server. Contact writes use https://{subdomain}.{crm-host}. Do not send /rest/people with the provisioning secret.


Environment variables

aventora-crm (server)

VariableRequiredDescription
PROVISIONING_SECRETYesBearer token for /auth/provision/* (server-side only)
APP_VERSIONYesValid semver (e.g. 1.20.0); drives workspace migrations. Never leave empty.
FRONTEND_URLMulti-workspaceApex URL, e.g. https://crm.aventora.ai
SERVER_URLYesAPI base; usually same as FRONTEND_URL
IS_MULTIWORKSPACE_ENABLEDProductiontrue for {subdomain}.crm.aventora.ai
AVENTORA_BASE_URLHub integrationEngagement Hub API base URL
AVENTORA_WEBHOOK_SECRETRecommendedValidates X-Aventora-Webhook-Secret on inbound webhooks
AVENTORA_HUB_AUTO_PROVISION_ENABLEDOptionalWhen true, enqueue Hub/domain provisioning after CRM workspace or member provisioning
AVENTORA_HUB_PROVISION_CALLBACK_URLWith auto-provisionAssistant URL, e.g. http://hub:8010/internal/crm-hub-provision
AVENTORA_HUB_PROVISION_CALLBACK_SECRETWith auto-provisionShared Bearer secret with Assistant (CRM_HUB_PROVISION_CALLBACK_SECRET)

Per workspace (not env): AVENTORA_API_KEY in Settings → Applications → Aventora Phone. AVENTORA_PERSON_WALL_ENABLED in Settings → Applications → Aventora (empty = inherit; false = opt out when PERSON_WALL=true).

Person wall (optional, server)

VariableDefaultDescription
PERSON_WALLfalseKill switch; when true, members see only their own Person records unless workspace opts out
PERSON_WALL_WORKSPACE_IDSemptyComma-separated workspace UUID allowlist (empty = all workspaces)
PERSON_WALL_WORKSPACE_IDemptyDeprecated single-UUID allowlist
PERSON_WALL_EXCLUDE_SYSTEM_RECORDSfalseHide integration-created persons from scoped members
PERSON_WALL_DEBUGfalseLog PersonWall scope decisions

Rollout: workspace:sync-aventora-variables, then workspace:backfill-person-created-by. See Person wall (optional, server).

Calling app (integrator / admin / domain-chatbot)

VariableRequiredDescription
CRM_API_URLYesApex URL for /auth/provision/*
CRM_PROVISIONING_SECRETYesMust match CRM PROVISIONING_SECRET
CRM_PUBLIC_BASE_URLContacts / URLsDefaults to CRM_API_URL
CRM_WORKSPACE_API_KEYContact writesWorkspace-scoped token for /rest/*

Laravel uses AVENTORA_CRM_URL and AVENTORA_CRM_PROVISIONING_SECRET.

TIPS (optional, server + worker)

VariableRequiredDefaultDescription
TIPS_CLIENT_IDFor syncOAuth client id
TIPS_CLIENT_SECRETFor syncOAuth secret
TIPS_API_BASE_URLNohttps://tipsadvisors.tipservices.ca/apiInclude /api
TIPS_SYNC_WORKSPACE_IDFor syncSingle workspace UUID
TIPS_SYNC_ENABLEDNofalseEnable scheduled cron
TIPS_SYNC_CRON_PATTERNNo0 * * * *BullMQ cron (hourly)

Deployment and upgrade

Production (Docker)

Containers run migrations and upgrade on startup (packages/twenty-docker/twenty/entrypoint.sh) unless DISABLE_DB_MIGRATIONS=true:

yarn command:prod cache:flush
yarn command:prod upgrade
yarn command:prod workspace:sync-aventora-sales-person-fields
yarn command:prod cache:flush

Manual upgrade after pulling a new image:

cd /path/to/compose # directory with docker-compose.yml and .env
docker compose pull
docker compose up -d --force-recreate server worker

# Or run explicitly:
docker compose exec server sh -c \
'yarn command:prod cache:flush && yarn command:prod upgrade && yarn command:prod cache:flush'
docker compose exec server yarn command:prod workspace:sync-aventora-variables

Health check:

curl -sf https://crm.example.com/healthz

Keep the worker container running for BullMQ (TIPS cron, outbound jobs).

Development

npx nx run twenty-server:platform:upgrade

Equivalent manual steps: build → cache:flushupgradeworkspace:sync-aventora-sales-person-fieldscache:flushworkspace:sync-aventora-variables.

What upgrade does

  1. Pending TypeORM core migrations
  2. Workspace-level data migrations for current APP_VERSION minor
  3. Updates core.workspace.version per workspace

Check workspace versions:

SELECT id, version, "displayName", "activationStatus"
FROM core.workspace
ORDER BY version NULLS FIRST;

New deploy checklist

  1. Update .env (APP_VERSION, URLs, secrets)
  2. docker compose up -d (server runs migrations + upgrade + cron register)
  3. Confirm curl …/healthz and worker Up
  4. workspace:sync-aventora-variables
  5. Existing workspaces with an industry preset: workspace:reapply-industry-preset (seeds new [preset:…] workflows and cockpit rules without changing preset id/profile)
  6. Review Settings → Workflows and Sales Cockpit → Automation Suggestions; activate templates you want live

Further deployment paths: Docker Hub deploy, Deploy on VPS.


Admin CLI commands

Run in production:

docker compose exec server yarn command:prod <command> [options]

Dev:

npx nx run twenty-server:command -- <command> [options]

Quick index

CommandPurpose
upgradePrimary migrator — run on every deploy
cache:flushFlush Redis; run around upgrade
workspace:sync-aventora-variablesEnsure Aventora app vars on all workspaces (non-destructive)
workspace:sync-aventora-sales-person-fieldsSales Cockpit Person fields (auto after upgrade in Docker)
workspace:seed-aventora-sales-cockpit-demoDemo signals on existing people
workspace:apply-industry-presetApply preset to existing workspace (first time or with overrides)
workspace:reapply-industry-presetRe-apply preset from each workspace’s stored industryPreset / industryProfile
tips:bootstrap-fieldsTIPS fields + hub role + RLS
tips:syncOne-off TIPS inbound sync
cron:register:allRegister background crons (incl. TIPS)
workspace:backfill-person-created-byBackfill person.createdBy for PERSON_WALL readiness

Demo sales signals

After upgrade and person-field sync:

docker compose exec server yarn command:prod workspace:seed-aventora-sales-cockpit-demo

Idempotent: adds demo signals on existing people in active workspaces. Does not create or delete CRM records.

Local dev (destructive full reset + demo signals):

npx nx database:reset twenty-server

Industry preset on existing workspace

Re-apply after upgrade (uses each workspace’s stored preset; no UUID or preset flags required):

docker compose exec server yarn command:prod workspace:reapply-industry-preset

Optional single workspace: add -w <workspace-uuid>. Idempotent: appliers only add missing [preset:…] workflows, rules, fields, etc.

First-time apply or change preset (requires workspace id; use --force if already applied):

docker compose exec server yarn command:prod workspace:apply-industry-preset \
-w <workspace-uuid> \
--industry-preset insurance \
--industry-profile general_insurance_advisor

Check state:

SELECT id, "displayName", "industryPreset", "industryProfile", "presetAppliedAt"
FROM core.workspace WHERE id = '<uuid>';

Provisioning and SSO

All routes: Authorization: Bearer <PROVISIONING_SECRET> on CRM_API_URL.

MethodPathPurpose
POST/auth/provision/resolveFind or create workspace + user
POST/auth/provision/login-tokenSSO token + workspaceUrl; optional page, avatar
POST/auth/provision/person-ownership-contextResolve workspace member + Person Wall scope for Hub contact creates
POST/auth/provision/userAdd user to existing workspace
POST/auth/provision/move-userMove user membership from one workspace to another (access only)
POST/auth/provision/workspaceCreate or reuse named workspace (supports industryPreset)
GET/auth/provision/workspace?subdomain= or ?displayName=Lookup workspace
GET/auth/provision/workspaces?activationStatus=ACTIVEList workspaces with hubConnected, adminEmail, assignedDomain
POST/auth/provision/link-workspaceWire Hub↔CRM; returns twentyApiKey
POST/auth/provision/demo-tenantDemo workspace + API key + Phone wiring
POST/auth/provision/sync-cockpit-action-mappingsPush Sales Cockpit mappings
GET/auth/provision/industry-preset-catalogPreset list for UIs
DELETE/auth/provision/workspace?subdomain=Hard-delete workspace (demo teardown)

SSO sequence

sequenceDiagram
participant Browser
participant App as Calling App Backend
participant CRM as aventora-crm

Browser->>App: Open CRM
App->>CRM: POST /auth/provision/resolve
CRM-->>App: workspaceId, userId, subdomain
App->>CRM: POST /auth/provision/login-token
CRM-->>App: workspaceUrl
App-->>Browser: Redirect to workspaceUrl

All provisioning calls must run on the backend. The browser only receives the final workspaceUrl.

Resolve

POST /auth/provision/resolve
{
"email": "agent@example.com",
"firstName": "Alex",
"lastName": "Agent",
"tenantSubdomain": "acme",
"tenantDisplayName": "Acme Inc",
"avatar": "https://cdn.example.com/avatars/user.png"
}

tenantSubdomain optional — omit for personal workspace mode.

Optional avatar — same rules as login-token.

Response includes wasCreated: { workspace, user }.

Login token

POST /auth/provision/login-token
{
"workspaceId": "...",
"email": "agent@example.com",
"firstName": "Alex",
"lastName": "Agent",
"engagementInitiatorPhone": "+15551234567",
"page": "/objects/people",
"avatar": "https://cdn.example.com/avatars/user.png"
}

page is optional — see Embedded page mode.

avatar is optional — a publicly accessible http or https image URL. CRM downloads the image, stores it as the workspace member profile picture, and uses it in the user profile and anywhere the member avatar is shown. Invalid URLs return HTTP 400. If the URL is unreachable or not an image, provisioning continues without an avatar (logged server-side).

Creates user + membership if missing. Redirect browser to workspaceUrl.

The returned workspaceUrl always includes aventoraSso=1 (CRM hides Log out for SSO sessions). When page is set, the URL also includes returnToPath and aventoraEmbedded=1.

Person ownership context (Hub contact creates)

Engagement Hub attributes CRM people to the initiating user by calling this endpoint before POST /rest/people.

POST /auth/provision/person-ownership-context
{
"workspaceId": "...",
"email": "agent@example.com",
"engagementInitiatorPhone": "+15551234567"
}

At least one of email or engagementInitiatorPhone is required. Phone matches userWorkspace.aventoraEngagementInitiatorPhone (same normalization as login-token provisioning). If the user is not yet a workspace member, CRM provisions them with the Aventora User role (same as SSO).

Response:

{
"workspaceMemberId": "...",
"displayName": "Jane Agent",
"personWallScopeFieldName": "tipsAgentId",
"personWallScopeValue": 42
}

personWallScopeFieldName / personWallScopeValue are included when Person Wall is enabled and PERSON_WALL_FIELD_NAME includes a legacy numeric field (e.g. TIPS tipsAgentId or tipsAgentId,createdBy). Hub copies both createdBy.workspaceMemberId and the scope field onto new people.

Engagement Hub env (same values as domain-chatbot provisioning):

VariableDescription
CRM_API_URLApex URL for /auth/provision/person-ownership-context
CRM_PROVISIONING_SECRETMust match CRM PROVISIONING_SECRET

Domains must have crm_workspace_id in domain account_settings (set when CRM is enabled in aventora-admin).

Embedded page mode (host-driven navigation)

Use this when your app (not CRM) owns navigation — e.g. a sidebar or submenu in aventora-admin that opens CRM in an iframe or new window.

Modepage parameterCRM behavior
Full CRMOmittedNormal CRM with left navigation and mobile bottom nav
Embedded pageProvidedLeft nav and mobile bottom nav hidden; user lands on the requested screen

In embedded mode:

  • Log out is hidden in CRM (user exits via your app)
  • Navigation is your responsibility — each submenu item should trigger a new SSO request with a different page
  • The user may still follow in-page links (e.g. open a record); CRM nav stays hidden for that session

How to call SSO with page

Laravel host app (authenticated):

GET /crm/sso?page=/objects/people
Query paramRequiredDescription
tenantNoCRM workspace subdomain. Omit for the user’s personal workspace.
pageNoFull internal CRM path (see Available pages). Omit for full CRM.
avatarNoPublic http/https image URL for the user’s CRM profile picture. Omit to leave unchanged.

Direct provisioning API (server-to-server):

POST /auth/provision/login-token
{
"workspaceId": "uuid",
"email": "user@example.com",
"page": "/objects/people",
"avatar": "https://cdn.example.com/avatars/user.png"
}

The same optional avatar field is supported on POST /auth/provision/resolve and POST /auth/provision/user.

SSO response (Laravel GET /crm/sso)

{
"url": "https://{subdomain}.crm.example.com/verify?loginToken=...&aventoraSso=1&returnToPath=%2Fobjects%2Fpeople&aventoraEmbedded=1",
"loginToken": "...",
"expiresAt": "2026-...",
"workspaceId": "...",
"subdomain": "...",
"userId": "...",
"page": "/objects/people"
}
FieldDescription
urlOpen this URL in iframe, popup, or redirect
loginTokenShort-lived token (also inside url)
expiresAtToken expiry (ISO timestamp)
workspaceIdCRM workspace ID
subdomainWorkspace subdomain
userIdCRM user ID
pageEcho of requested page (only when provided)

Your frontend should open url — no extra client-side URL building is required.

page parameter rules

  1. Format: full internal CRM path, starting with /
    • Valid: /cockpit, /objects/people, /settings/profile
    • Invalid: cockpit, https://..., //evil.com
  2. Validation: invalid paths return HTTP 400 from login-token
  3. URL encoding: when passing page as a query param, encode it: page=%2Fobjects%2Fpeople
  4. Permissions: a valid path may still show an empty or restricted view if the user lacks CRM permissions
  5. Custom objects: use /objects/{pluralName} where {pluralName} is the object’s API plural name in that workspace

Blocked paths (return 400)

Do not use auth, onboarding, or sign-up routes:

  • /welcome, /verify, /verify-email
  • /create/*, /invite-team, /plan-required, /book-call*
  • /reset-password/*
  • / (root alone)

Available page values

Main app screens

Screenpage valueNotes
Sales Cockpit/cockpitDefault home when Sales Cockpit is enabled
People (list)/objects/peopleStandard CRM object
Companies (list)/objects/companiesStandard CRM object
Opportunities (list)/objects/opportunitiesStandard CRM object
Tasks (list)/objects/tasksStandard CRM object
Notes (list)/objects/notesStandard CRM object
Dashboards (list)/objects/dashboardsIf enabled in workspace
Workflows (list)/objects/workflowsIf enabled in workspace

Record detail pages (optional deep links)

Pattern: /object/{singularName}/{recordId}

Screenpage value
Person record/object/person/{uuid}
Company record/object/company/{uuid}
Opportunity record/object/opportunity/{uuid}

Replace {uuid} with the CRM record ID.

Settings screens (prefix /settings/)

Settings areapage value
Profile/settings/profile
Experience (theme/locale)/settings/experience
Connected accounts/settings/accounts
Account emails/settings/accounts/emails
Account calendars/settings/accounts/calendars
Workspace general/settings/general
Data model/settings/objects
Members/settings/members
Roles/settings/roles
Domains/settings/domains
Billing/settings/billing
APIs & Webhooks/settings/api-webhooks
Apps/settings/applications
AI/settings/ai
Security/settings/security
Admin panel/settings/admin-panel
Updates/settings/updates

Settings sub-pages with IDs (roles, API keys, etc.) follow the same pattern, e.g. /settings/roles/{roleId}.

Custom workspace objects

/objects/{objectPluralName}
/object/{objectSingularName}/{recordId}

Example: custom object plural deals/objects/deals

Your app menuSSO call
CRM Home (full)GET /crm/sso
Sales CockpitGET /crm/sso?page=/cockpit
PeopleGET /crm/sso?page=/objects/people
CompaniesGET /crm/sso?page=/objects/companies
OpportunitiesGET /crm/sso?page=/objects/opportunities
My profileGET /crm/sso?page=/settings/profile

Each click: (1) call /crm/sso with the chosen page, (2) open returned url. Switching sections = new SSO call, not in-CRM navigation.

Embedded vs full CRM

FeatureWithout pageWith page
Left navigationVisibleHidden
Mobile bottom navVisibleHidden
Log out in CRMHidden (SSO session)Hidden (SSO session)
Landing screenCRM default homeYour page path
Who navigatesUser (CRM nav)Your app (submenu + SSO)

Verify embedded SSO

  1. GET /crm/sso → full CRM with navigation
  2. GET /crm/sso?page=/cockpit → Cockpit only, no nav
  3. GET /crm/sso?page=/objects/people → People list, no nav
  4. GET /crm/sso?page=/welcome400 (invalid path)
  5. Switch submenu → new SSO call with different page → correct screen each time

Existing workspace shortcut

When crm_workspace_id is already stored (aventora-admin after CRM enable), skip resolve and call login-token only with that workspaceId.

Add user explicitly

POST /auth/provision/user with workspaceId + email. For SSO, login-token alone is usually enough.

Provision workspace with preset

POST /auth/provision/workspace
{
"adminEmail": "admin@example.com",
"subdomain": "acme",
"displayName": "Acme Insurance",
"industryPreset": "insurance",
"industryProfile": "general_insurance_advisor"
}

Idempotent: if subdomain exists, adds/finds admin user and returns existing workspace.


Workspaces, users, and industry presets

When is a workspace created?

FlowWho picks workspaceCreates workspace?
resolve + tenantSubdomainClient passes subdomainYes, if subdomain new
resolve without tenantSubdomainCRM per emailYes, personal workspace if none exists
login-token with workspaceIdCaller (stored ID)Never
provision/workspace or demo-tenantExplicit API callYes, if subdomain new
TIPS tips:syncTIPS_SYNC_WORKSPACE_ID envNever
aventora-admin SSOcrm_workspace_id in domain settingsNever at login time

Resolve decision tree

User already has a workspace membership:

  • Returns that workspace (no create)
  • If requested tenantSubdomain differs → 400

New user + tenantSubdomain provided:

  • Subdomain exists → join workspace, create user if needed
  • Subdomain missing → create shared workspace + user as admin

New user + no tenantSubdomain:

  • Personal workspace for email exists → reuse
  • Else → create personal workspace + user

Hard rules:

  • One workspace per user through provisioning (multi-membership → 400)
  • Cannot move user to another workspace via SSO (400) — use move-user instead
  • User exists with no membership → 400 (orphan; manual cleanup)

Move user between workspaces

POST /auth/provision/move-user moves a user's login membership from workspace A to workspace B. It does not migrate CRM records (People, engagements, cockpit history, etc.) — those stay in the source workspace.

Requirements:

  • User must belong to exactly one workspace (the from workspace)
  • Source workspace must have at least one other member (cannot move the only member out)
  • User cannot be the only admin in the source workspace unless another admin is assigned first
  • Target workspace must be active
POST /auth/provision/move-user
{
"email": "agent@example.com",
"fromWorkspaceSubdomain": "tenant-a",
"toWorkspaceSubdomain": "tenant-b",
"firstName": "Alex",
"lastName": "Agent",
"avatar": "https://cdn.example.com/avatars/agent.png"
}

Use fromWorkspaceId / toWorkspaceId instead of subdomains when IDs are already stored.

Response:

{
"userId": "uuid",
"email": "agent@example.com",
"fromWorkspaceId": "uuid",
"fromSubdomain": "tenant-a",
"toWorkspaceId": "uuid",
"toSubdomain": "tenant-b",
"userRestored": false
}

userRestored is true when the core user row was soft-deleted and restored during the move.

After a successful move, update external systems (crm_workspace_id, SSO login-token workspaceId) to the to workspace. Issue a new login-token for the target workspace on next SSO.

SituationResult
User not found404
User not in from workspace400
User already in to workspace400 (use login-token only)
User in multiple workspaces400
Only member of from workspace400
Only admin of from workspace400
from and to are the same400
Target workspace inactive400

Industry presets

Presets apply once at workspace activation when industryPreset is set on the workspace row. They add labels, custom fields, views, pipelines, dashboards, CRM workflow templates (DRAFT), and Sales Cockpit automation rules.

Aventora workflow templates (v1)

Industry presets seed inactive Aventora automation in two layers. Templates are not created by upgrade alone on workspaces that already have presetAppliedAt set — run workspace:reapply-industry-preset after deploying a build that includes workflow seeding.

LayerWhere in CRMHow to findDefault stateUser activates
Twenty CRM WorkflowsWorkflows moduleSettings → Workflows — names start with [preset:av_wf_…]DRAFTOpen template → review → Activate
Sales Cockpit rulesCore aventoraAutomationRule tableSales Cockpit (/cockpit) → Automation Suggestions panelDRAFT / disabledEnable rule; optional Auto-run when triggered

Manual CRM workflows also appear on People records (single-record or bulk selection) via the workflow/command menu after activation.

CRM workflow templates (all industries)

KeyUI name (prefix [preset:…])TriggerHub action
av_wf_manual_engage_personEngage Person (Aventora)Manual, single personForm (channel + instruction) → engage
av_wf_new_person_say_helloNew Person: Say Hello (SMS)person.createdSAY_HELLO SMS
av_wf_new_person_qualify_callNew Person: Qualify (Call)person.createdCALL_LEAD phone
av_wf_bulk_sms_checkinBulk SMS: Check InterestManual bulk peopleCHECK_INTEREST SMS
av_wf_bulk_reconnectBulk SMS: ReconnectManual bulk peopleRECONNECT SMS
av_wf_bulk_booking_linkBulk SMS: Send Booking LinkManual bulk peopleSEND_BOOKING_LINK SMS

Workflow steps use custom action types START_AVENTORA_ENGAGEMENT (single person) and START_AVENTORA_ENGAGEMENT_BULK (bulk). Both call the shared AventoraEngagementExecutionService, which respects doNotContact on the person and uses workspace Aventora app variables + cockpit action mappings.

Cockpit automation templates

KeyNameSignalsDefault action
av_cockpit_missed_callMissed Call Follow-UpMISSED_CALLCALL_LEAD
av_cockpit_no_replyNo Reply Follow-UpNO_REPLYSEND_FOLLOW_UP

When Auto-run is enabled on an active rule, the cockpit execution path applies cooldown and max executions per lead per day guards before calling Hub.

GraphQL (cockpit rules): updateAventoraAutomationRule — toggle enabled and autoExecute from the Automation Suggestions UI.

Compliance

Auto workflows (person.created, cockpit auto-run) can contact leads without a manual click. Review every template before activation. People marked do not contact are skipped by the execution service.

industryPresetExample profiles
real_estatebuyer_agent, listing_agent, broker, …
insurancepersonal_lines, general_insurance_advisor, …
mortgagemortgage_agent, broker_owner, …
financial_advisorwealth_advisor, practice_owner, …
genericdefault

Important: POST /auth/provision/resolve does not accept industryPreset. Personal and tenant workspaces created via resolve alone get no industry preset (generic CRM + standard Aventora fields).

To control preset:

  1. Use POST /auth/provision/workspace or demo-tenant with industryPreset + industryProfile
  2. Use onboarding UI (self-serve signup)
  3. Run workspace:apply-industry-preset after the fact (first time or to change preset/profile)
  4. Run workspace:reapply-industry-preset after upgrades to backfill new template artifacts on workspaces that already have a preset

TIPS: One shared workspace is created with insurance preset before sync. tips:sync only creates users in that workspace — not new workspaces or presets per agent.

Color theme presets

Workspace admins can choose an accent color theme for the whole CRM UI. This is separate from industry presets (which seed CRM data and workflows). Color themes only change accent colors — buttons, links, highlights, and related UI chrome. They do not switch light vs dark mode.

CRM has two independent appearance controls:

SettingWhat it changesScopeWhere in CRM
Color theme (themePreset)Accent palette (Classic, Ocean, Forest, …)Entire workspace — all members see the same accentsSettings → Workspace → General → Color theme
Light / Dark / System (colorScheme)Light mode, dark mode, or follow OSPer user — each member chooses their ownTop workspace menu → Theme, or Settings → User → Experience → Appearance

The workspace dropdown Theme submenu (System / Dark / Light) is the same personal colorScheme setting as User → Experience — not the workspace color theme.

Workspace color theme changes apply immediately for all members after save. Default for new and existing workspaces (post-migration) is classic (Twenty indigo).

Available presets

themePreset valueLabelAccent palette
classicClassicIndigo (Twenty default)
oceanOceanTeal
forestForestJade
sunsetSunsetOrange
violetVioletViolet
roseRoseCrimson
midnightMidnightIris

Valid values are enforced on updateWorkspace; invalid IDs are rejected.

API and storage

  • Column: core.workspace."themePreset" (varchar, default 'classic')
  • Migration: 1782000000002-add-workspace-theme-preset.ts (runs with normal upgrade)
  • GraphQL: updateWorkspace(data: { themePreset: "ocean" }) on workspace metadata API
  • Permission: PermissionFlagType.WORKSPACE (same as workspace name/logo)

Check current value:

SELECT id, "displayName", "themePreset"
FROM core.workspace
WHERE id = '<uuid>';

Programmatic update (admin session or API key with workspace settings access):

mutation UpdateWorkspaceTheme {
updateWorkspace(data: { themePreset: "ocean" }) {
id
themePreset
}
}

Deploy note

After upgrading to a build that includes color themes:

  1. Ensure core migrations have run (yarn command:prod upgrade) — adds core.workspace.themePreset.
  2. No extra CLI command is required; admins pick a preset in Settings → Workspace → General.

Color themes and industry preset workflows ship in the same CRM build but are configured independently.


Sales Cockpit

Signal-driven sales layer on Person records: ingest signals → scores → suggested actions → /cockpit dashboard and Person Sales tab.

Enablement checklist

  1. Deploy with upgrade (Sales Cockpit migrations)
  2. workspace:sync-aventora-sales-person-fields and workspace:sync-aventora-variables
  3. Wire Hub↔CRM: AVENTORA_API_KEY, webhooks, AVENTORA_WEBHOOK_SECRET
  4. aventora-admin: Enable Aventora CRM; configure cockpit action mappings
  5. Confirm IS_SALES_COCKPIT_ENABLED on workspace
  6. Optional demo data: workspace:seed-aventora-sales-cockpit-demo

Feature flags

FlagEffect
IS_SALES_COCKPIT_ENABLED/cockpit nav, default home route
IS_SUGGESTED_ACTIONS_ENABLEDReserved

Dashboard panels

Priority Leads, Suggested Actions, Reconnect Recent, Reconnect Oldies, Stale Leads, Hot Leads, Recently Engaged, Leads Requiring Follow-up, Team Activity, Automation Suggestions (enable preset rules and optional Auto-run when triggered; respects cooldown and daily max per lead).

Signal sources

SourceHow
Hub webhooksEngagement outcomes → signals
RESTPOST /rest/sales-intelligence/signals
GraphQLcreateAventoraPersonSignal
Demoworkspace:seed-aventora-sales-cockpit-demo

Cockpit action → Hub mappings

Configured in aventora-admin → Account Settings → CRM Sync when provider is Aventora CRM.

Stored in domain-chatbot twenty_cockpit_action_mappings, synced to CRM app var AVENTORA_COCKPIT_ACTION_MAPPINGS via POST /auth/provision/sync-cockpit-action-mappings.

Hub accepts informational, conversational, appointment_booking, confirmational — not raw cockpit action names like ESCALATE_TO_SALES.

Cockpit APIs

  • GraphQL: aventoraSalesCockpitSummary, aventoraPersonIntelligence, …
  • REST: GET /rest/sales-intelligence/cockpit-summary, POST /rest/sales-intelligence/signals
  • Execute: POST /rest/aventora/start-engagement with cockpitActionType

Sales Cockpit troubleshooting

SymptomLikely causeFix
"Sales cockpit is not available yet."GraphQL query failed (not empty data)Browser DevTools → Network → GetAventoraSalesCockpitSummary → read errors[0].message. Check server logs.
SQL error on load (e.g. missing column)Migrations or person fields not syncedupgrade, workspace:sync-aventora-sales-person-fields
Panels show "No leads to show"Query succeeded, no signalsworkspace:seed-aventora-sales-cockpit-demo or ingest signals
Execute 400, invalid Hub typeMappings not syncedAdmin → Sync to CRM
Feature missingFlag offIS_SALES_COCKPIT_ENABLED
No [preset:…] workflows after upgradeWorkspace already had presetAppliedAt; templates not backfilledworkspace:reapply-industry-preset
reapply-industry-preset errors on workflow insertOlder workspace workflow metadataUpgrade to latest build (applier omits legacy actor columns); re-run workspace:reapply-industry-preset

Debug GraphQL: failed cockpit load always means backend error. Empty data still shows panel grid with "No leads to show."


CRM-first Hub provisioning (existing CRM workspaces)

Automatic (CRM-triggered)

When AVENTORA_HUB_AUTO_PROVISION_ENABLED=true on aventora-crm, provisioning a workspace or adding a workspace member enqueues a BullMQ job that calls Aventora Assistant POST /internal/crm-hub-provision. CRM holds only the callback URL + shared secret; domain-chatbot super-admin credentials and telephony env stay on Assistant.

CRM envAssistant env
AVENTORA_HUB_AUTO_PROVISION_ENABLED=trueCRM_HUB_PROVISION_CALLBACK_SECRET (same value as CRM secret)
AVENTORA_HUB_PROVISION_CALLBACK_URLDOMAIN_CHATBOT_*, PROVISION_INBOUND_PHONE, Twilio vars
  • New workspace (via /auth/provision/* createWorkspaceWithAdmin): job action=full after ~60s delay (deduped per workspace).
  • New workspace member: job action=sync_members after ~30s delay (skipped server-side if Hub not linked yet).

Requires CRM worker process running (same as TIPS sync). Failures retry up to 3 times; CRM provisioning itself never fails because of Hub auto-provision.

Manual batch (ops backfill)

When CRM workspaces already exist but domain-chatbot, Engagement Hub, and CRM↔Hub wiring are missing, run the aventora-assistant script from aventora-phone:

cd aventora-phone
python scripts/provision_hub_from_crm_workspaces.py --dry-run

python scripts/provision_hub_from_crm_workspaces.py --execute \
--inbound-phone +14167078887 \
--twilio-account-sid "$PROVISION_TWILIO_ACCOUNT_SID" \
--twilio-api-key "$PROVISION_TWILIO_API_KEY" \
--twilio-api-secret "$PROVISION_TWILIO_API_SECRET"

# Classic auth token mode (instead of API key):
python scripts/provision_hub_from_crm_workspaces.py --execute \
--inbound-phone +14167078887 \
--twilio-account-sid "$PROVISION_TWILIO_ACCOUNT_SID" \
--twilio-auth-token "$PROVISION_TWILIO_AUTH_TOKEN"

Hub stores API key credentials as twilio_account_sid (AC…), twilio_api_key_sid (SK…), and twilio_auth_token (secret). Classic mode uses twilio_account_sid + twilio_auth_token only. Both modes are supported at runtime via create_twilio_client.

The shared --inbound-phone is written to inbound settings and, by default, to all Twilio outbound fields: twilio_phone_number, twilio_sms_number, twilio_whatsapp_number, and display_number (caller ID). Use --twilio-phone-number only when outbound must differ from inbound.

The script:

  1. Calls domain-chatbot GET /regenerate-index/provision-crm-workspaces (super-admin; CRM credentials stay on domain-chatbot).
  2. For each disconnected workspace, calls domain-chatbot POST /regenerate-index/provision-from-crm-workspace to create the domain, Hub account, shared inbound/Twilio defaults, and link-workspace CRM credentials.
  3. For already-linked workspaces, use POST /regenerate-index/sync-crm-workspace-members or provision_hub_from_crm_workspaces.py --sync-members-only --execute to backfill domain-chatbot + Hub users from CRM hubSyncMemberEmails.

Required env (Hub / aventora-phone)

VariableUsed by
DOMAIN_CHATBOT_API_URL (or DOMAIN_CHATBOT_URL)Workspace list + reverse provision API
DOMAIN_CHATBOT_SUPER_ADMIN_TOKEN or SUPER_ADMIN_USERNAME + SUPER_ADMIN_PASSWORDdomain-chatbot super-admin auth
CRM_API_URL, CRM_PROVISIONING_SECRETPerson ownership context for Hub-created CRM contacts
DATABASE_URL (optional)Hub accounts.domain_name lookup in dry-run summary
PROVISION_INBOUND_PHONE, PROVISION_TWILIO_ACCOUNT_SID, PROVISION_TWILIO_AUTH_TOKEN or PROVISION_TWILIO_API_KEY + PROVISION_TWILIO_API_SECRETCLI fallbacks for telephony
CRM_HUB_PROVISION_CALLBACK_SECRETAuthenticates CRM POST /internal/crm-hub-provision (same value as CRM AVENTORA_HUB_PROVISION_CALLBACK_SECRET)

CRM CRM_API_URL and CRM_PROVISIONING_SECRET are required on Engagement Hub for person ownership resolution (Hub calls POST /auth/provision/person-ownership-context when creating CRM contacts). They remain required on domain-chatbot for workspace list, link-workspace, and demo provisioning.

Required env (domain-chatbot)

VariableUsed by
CRM_API_URL, CRM_PROVISIONING_SECRETCRM workspace list + link-workspace

Inbound phone and Twilio credentials are operator-supplied (CLI or env), applied to every workspace on first provision. Per-account overrides are done later in aventora-admin Account Settings.

Prerequisites

  • CRM deployed with migrations and workspace:sync-aventora-variables
  • domain-chatbot and Engagement Hub running with service keys configured
  • CRM workspace subdomain (or AVENTORA_ASSIGNED_DOMAIN) must be a valid domain slug: [a-z0-9-]{3,30}

Demo workspace provisioning

Ephemeral demos provision a CRM workspace via domain-chatbot + POST /auth/provision/demo-tenant.

domain-chatbot env

VariableDescription
CRM_API_URL, CRM_PROVISIONING_SECRETCRM provisioning
CRM_PUBLIC_BASE_URLWorkspace URL builder
CRM_WEBHOOK_SECRETMust match CRM AVENTORA_WEBHOOK_SECRET
DEMO_ENABLE_CRMDefault true; false to skip
HUB_API_URL, HUB_API_KEYDemo engagement

CRM endpoints

  • POST /auth/provision/demo-tenant — workspace + twentyApiKey + Phone wiring (supports industryPreset)
  • DELETE /auth/provision/workspace?subdomain= — teardown

Verification

  1. Create demo invitation in admin
  2. Confirm demo_config has crm_workspace_id, crm_subdomain
  3. Open demo live page → CRM tab
  4. Run engagement demo → contact appears in CRM
  5. On revoke/expiry → workspace deleted

Demo slugs capped at 30 characters for CRM subdomain validation.


TIPS Services sync

Optional: sync TIPS Advisors agents → CRM users; clients → people. Ships on main; enable with TIPS_* env.

Setup (Tip Services)

  1. Create workspace (onboarding: Insurance Brokerage preset, or workspace:apply-industry-preset)
  2. Set TIPS_SYNC_WORKSPACE_ID on server and worker
  3. tips:bootstrap-fields (fields + Member (Aventora Hub) role)
  4. tips:sync
  5. Optional schedule: TIPS_SYNC_ENABLED=true, recreate server or cron:tips:sync

Inbound sync

For each TIPS agent with crmEnabled:

  • ensureUserInWorkspaceForTipsSync — same primitive as SSO findOrCreateUserInWorkspace (+ password sync, role from crmHubEnabled)
  • Updates workspaceMember tips* fields
  • Upserts Person records for clients

TIPS vs client SSO

Client SSO (resolve)TIPS sync
WorkspacePer tenantSubdomain or personalFixed TIPS_SYNC_WORKSPACE_ID
Creates workspaceSometimesNever
Industry presetOnly via provision/workspaceOn pre-built workspace
TriggerUser clicks Open CRMCron / CLI

Verify scheduled sync

docker compose ps worker
docker compose logs server 2>&1 | grep -i TipsSync
docker compose logs worker --since 24h | grep -i tips

Laravel plugin

Building your own Laravel integration? Use CRM_LARAVEL_INTEGRATION.md — the shareable guide for custom HTTP clients (provisioning, SSO, page, avatar). This section covers the official Composer package only.

Package: aventora-crm/laravel-plugin/

composer require aventora/crm-laravel
php artisan vendor:publish --tag=crm-config
AVENTORA_CRM_URL=https://crm.example.com
AVENTORA_CRM_PROVISIONING_SECRET=...
AVENTORA_CRM_DEFAULT_TENANT=acme # optional shared subdomain

Route: GET /crm/ssoresolvelogin-token → open CRM.

Optional query param page — full CRM path for embedded page mode (hides CRM nav, deep-links to screen):

GET /crm/sso?page=/objects/people
GET /crm/sso?tenant=acme&page=/cockpit

Response JSON includes url (ready to open), plus page when requested. See SSO response for field list.

Health check:

php artisan crm:health-check --email=demo@local.test --tenant=acme

Blade:

@include('crm::components.button', ['tenant' => 'acme', 'label' => 'Open CRM'])

Contacts and workspace API keys

Base URL: https://{subdomain}.{crm-host}

POST /rest/people
Authorization: Bearer <CRM_WORKSPACE_API_KEY>
Content-Type: application/json

{
"name": { "firstName": "Jordan", "lastName": "Client" },
"emails": { "primaryEmail": "jordan@example.com", "additionalEmails": [] },
"phones": {
"primaryPhoneNumber": "5551234567",
"primaryPhoneCountryCode": "US",
"primaryPhoneCallingCode": "+1",
"additionalPhones": []
}
}

API keys from link-workspace, demo-tenant, or CRM Settings → API keys.


Webhooks

Hub → CRM: POST https://{subdomain}.{host}/rest/aventora/webhook with X-Aventora-Webhook-Secret.

CRM → Hub: per-workspace AVENTORA_API_KEY + server AVENTORA_BASE_URL.


Troubleshooting

Provisioning / SSO

# 1. Right host?
curl -sS "https://CRM_HOST/healthz"

# 2. Route exists? (expect 401, not 404)
curl -sS -X POST "https://CRM_HOST/auth/provision/resolve" \
-H "Authorization: Bearer test" \
-H "Content-Type: application/json" \
-d '{"email":"debug@test.example","tenantSubdomain":"test"}'

# 3. Valid secret (expect 200)
curl -sS -X POST "https://CRM_HOST/auth/provision/resolve" \
-H "Authorization: Bearer YOUR_SECRET" \
-H "Content-Type: application/json" \
-d '{"email":"debug@test.example","tenantSubdomain":"test"}'
HTTPMeaning
404 on /auth/provision/*Wrong host, old image, or proxy path
401Wrong PROVISIONING_SECRET
400 on resolveWorkspace conflict, multi-workspace user, orphan user
400 on move-userWrong membership state, only member/admin, inactive target workspace

Never expose provisioning secret or workspace API keys in browser JavaScript.

Error handling checklist

  • 404 on provision routes → hostname/deploy, not secret
  • 401 → secret mismatch
  • 400 on resolve → tenant/user conflict
  • 404 on login-token → unknown workspaceId
  • 400 on login-token → inactive workspace, or invalid page path (embedded mode)
  • User does not have permission when changing color theme → role lacks Workspace settings permission, or server build missing themePreset in workspace field permissions; grant Workspace on the role or upgrade server

Code map

AreaPath
Provisioningaventora-crm/.../provisioning/
Sales intelligenceaventora-crm/.../sales-intelligence/
Engage by Aventoraaventora-crm/.../aventora/
Cockpit UIaventora-crm/.../pages/sales-cockpit/
TIPS syncaventora-crm/.../modules/tips-sync/
Workspace presetsaventora-crm/.../modules/workspace-presets/
Preset CRM workflows.../constants/aventora-preset-crm-workflows.constant.ts, .../appliers/preset-crm-workflows.applier.ts
Preset cockpit rules.../constants/aventora-preset-cockpit-rules.constant.ts, .../appliers/preset-automation-rules.applier.ts
Reapply preset CLI.../database/commands/reapply-workspace-industry-preset.command.ts
Workflow Aventora actions.../workflow/workflow-executor/workflow-actions/aventora-engagement/
Engagement execution.../aventora/aventora-engagement-execution.service.ts
Color theme presetsaventora-crm/packages/twenty-front/src/modules/ui/theme/ (definitions, picker, apply effect); SettingsWorkspace.tsxColor theme; core.workspace.themePreset on server
Laravel pluginaventora-crm/laravel-plugin/
Admin CRM SSOaventora-admin/app/api/phone/account-settings/crm-sso/
Demo provisiondomain-chatbot/LLM_full/demo_invitations/crm_provision.py
Cockpit mappings UIaventora-admin/components/phone/cockpit-action-mappings-editor.tsx

Engineer code index (fork internals): aventora-crm/docs/aventora-customizations.md.


Changelog

DateChange
2026-06-14move-user provisioning: POST /auth/provision/move-user moves a user’s workspace membership (access only; CRM data stays in source workspace).
2026-06-13Laravel integration guide: new CRM_LARAVEL_INTEGRATION.md for custom Laravel clients (provisioning, SSO, navigation, avatar).
2026-06-13SSO avatar: optional avatar on resolve, login-token, provision/user, and GET /crm/sso; CRM downloads the image and sets workspace member profile picture.
2026-06-13SSO embedded page mode: optional page on login-token and GET /crm/sso; CRM hides nav and deep-links via returnToPath + aventoraEmbedded=1; documented available page paths and host submenu pattern.
2026-06-09Preset Aventora workflows v1: six DRAFT CRM workflow templates + two cockpit automation rules via industry presets; START_AVENTORA_ENGAGEMENT / _BULK workflow actions; workspace:reapply-industry-preset CLI; cockpit Enable / Auto-run UX; expanded UI paths and deploy checklist in this doc.
2026-06-09Workspace color theme presets (themePreset): seven accent palettes; documented vs per-user Light/Dark/System (colorScheme); admin UI, workspace menu shortcut, API/SQL, permissions.
2026-06-09Consolidated integration, Sales Cockpit, demo, upgrade, CLI, TIPS, and preset docs into this file.
2026-06-08Sales Cockpit: Reconnect Recent / Oldies panels.
2026-05-30Sales Cockpit action → Hub mappings via admin.