Twenty CRM ↔ Aventora Engagement — End-to-End Integration Plan
Document version: 1.3
Status: Implemented (Phases 1–4); Phase 5 (webhook retries) in progress
Scope: Twenty CRM (twenty), Aventora Phone (aventora-phone)
0. What is built (implementation summary)
Aventora-phone (aventora-phone)
- Twenty as CRM target: Account
twenty_webhook_url/twenty_webhook_secret(preferred) or envTWENTY_WEBHOOK_URL/TWENTY_WEBHOOK_SECRETfor legacy deployments. - Hub-originated calls (inbound + outbound): When
crm_sync_provider=aventora_crmandtwenty_sync_contacts=true,services/crm_sync/call_crm_sync.pyresolves or creates the Twenty person by phone/email at call start, setsengagement_idto the conversationcall_sidwhen none exists, mirrors an optional phoneengagementsrow, and POSTsengagement.created/engagement.updatedto Twenty. - When webhooks are sent: On CRM bootstrap (
engagement.created), call pause/resume/cancel, call complete, and call log updates (debounced per call). Payload built viaget_engagement_status()when a phone engagement row exists, otherwise fromcall_logs+call_history+call_transcription. - Payload shape:
engagement_id,person_id(CRM person UUID),status,outcome,summary,started_at,completed_at,history,transcript,channel,type,contact_identifier,direction(inbound/outbound),call_sid,domain_name, optionalworkspace_id,sent_at,delivery_type,event_type. Header:X-Aventora-Webhook-Secretwhen secret is set. - Summary field: Short string only: from call result (
result_json.summary), or"Outcome: {outcome}"when outcome exists, or last-resort"Status: {status}."when there is no history/transcript. Full detail is in the history and transcript arrays; Twenty (and other CRMs) use those for display. - History & transcript: Full JSON lists sent as-is; no pre-built long summary on Aventora side. Same pattern as other CRM integrations.
- Pause/resume in history: Pause and resume are written to the
call_historytable (as well ascall_logs.historyJSONB) so they appear in engagement history and in the payload. - Logging: Compact payload log when sending to CRMs (engagement_id, status, outcome, summary, sent_at, history_count, transcript_count, target list).
Twenty (twenty-server)
- Webhook endpoint:
POST /rest/aventora/webhook(public route; no JWT). ValidatesX-Aventora-Webhook-SecretagainstAVENTORA_WEBHOOK_SECRETwhen set. - Lookup / upsert: By
engagement_id. If the row is missing (hub-originated call), Twenty createsAventoraEngagementwhenperson_idis present and workspace is resolved viaworkspace_idhint,domain_name→AVENTORA_ASSIGNED_DOMAIN, or scanning workspaces until the person UUID is found. - Stored fields:
status,outcome,summary,startedAt,completedAt,history(jsonb),transcript(jsonb),webhookSentAt. Only fields present in the payload are updated (e.g.summary/outcome/history/transcriptwhen key is present). - Entity & DB:
AventoraEngagementEntityincore.aventoraEngagementwith columns for all above; migration1776000000000-add-aventora-engagement-webhook-fieldsadds outcome, summary, completedAt, history, transcript, webhookSentAt. - Idempotency: Newer
sent_atrequired to apply updates; older payloads are ignored. - Logging: Compact received-payload log (engagement_id, status, outcome, summary, sent_at, history_count, transcript_count).
End-to-end flow
- From Twenty UI: User starts engagement → Twenty calls
/integration/start→ AventoraEngagement row created withengagementId,personId,workspaceId. - From hub (inbound/outbound): Call log created → CRM bootstrap resolves/creates person →
engagement_id=call_sid→engagement.createdwebhook → Twenty upserts AventoraEngagement on the Person. - Aventora runs the call; on status changes it POSTs
engagement.updatedwith full history and transcript. - Twenty merges into AventoraEngagement; the Person Aventora section lists engagements with expandable history and transcript (no admin login required when CRM sync is enabled).
1. Goal
- User in Twenty uses “Engage By Aventora” on a Person record to start an engagement (after choosing channel, mode, instruction, and contact if multiple).
- System calls Aventora POST /integration/start and starts the engagement (phone, SMS, or email).
- Aventora sends engagement details back to Twenty via webhook; they appear under a dedicated Aventora section on the Person record (list of engagements with history + transcripts). One active engagement per person at a time.
2. Design Decisions (Final)
These decisions are locked before implementation.
| # | Topic | Decision |
|---|---|---|
| 1.1 | Start API | REST (e.g. POST /rest/aventora/start-engagement). |
| 1.2 | Multiple contacts / params | When user clicks “Engage By Aventora”, show a modal to collect: Channel (phone, SMS, email), Mode (Informational, Conversational, …), Instruction, and other required fields per channel/type. If person has multiple phones/emails, user selects which contact to use. Default values for channel, mode, instruction, etc. are configurable so the user doesn’t have to fill every time. |
| 1.3 | No phone / no email | If person has no phone: they can only be engaged by email (if they have email). If they have neither phone nor email, they cannot be engaged (button disabled or hidden, or error on click). |
| 1.4 | Channel & type | Covered by 1.2 (user chooses in modal; defaults from config). |
| 1.5 | Aventora API key | Same as today (existing header/format used by aventora-phone for call_management). |
| 2.1 | Sync mechanism | Webhook: Aventora POSTs to Twenty when engagement updates. |
| 2.2 | Webhook lookup | Engagement id is returned to Twenty when engagement is successfully started and stored in Twenty (AventoraEngagement row with engagementId, personId, workspaceId). When the webhook fires, payload contains engagement_id; Twenty looks up the record by engagement_id to get person and workspace. No reliance on source_org_id/source_record_id in webhook for lookup. |
| 2.3 | Workspace identification | Derived from the engagement record stored at start (see 2.2). |
| 3.1 | Object visibility | Easier approach first: AventoraEngagement is hidden/internal (used only to back the Aventora section). If needed later, expose as a listed object. |
| 3.2 | Object name | AventoraEngagement. |
| 3.3 | History & transcript | Stored as JSON on the AventoraEngagement record. |
| 3.4 | One engagement = | One engagement = one call_log in Aventora (same model as Aventora). One row per engagement_id; update in place when webhook sends status/history/transcript. |
| 4.1 | Aventora section placement | Yes: dedicated Aventora section on the Person record (tab or in-page block). |
| 4.2 | Section content | Both: list of engagements with latest expanded/summarized at top (option c). |
| 4.3 | “Engage By Aventora” button | Both: in command menu and in the Aventora section (e.g. “Start new engagement”). |
| 5.1 | Who can start | Configurable via Aventora configuration screen: same place where user sets API key, domain, default channel, default type/mode, default instruction, etc. Roles that can start (and view) engagements are configured there. |
| 5.2 | Who can view Aventora section | Same permission as who can start. |
| 5.3 | Aventora base URL | Server-level: stored in .env of the Twenty server (not per workspace). |
| 5.4 | Feature flag | Yes: feature flag/setting to enable/disable Aventora integration per workspace. |
| 6.1 | Duplicate / concurrent engagements | One active engagement per person at a time. If the person has an ongoing engagement (not completed), a new engagement cannot be started until it is completed. |
| 6.2 | Engagement not found (e.g. 404 from Aventora) | Show as “Broken” (or equivalent) in the Aventora section. |
| 6.3 | Webhook retries | Aventora should retry (e.g. every n seconds, m times, then a way to restart/retry). Backlog, not priority for first release. |
| 7.1 | Channels (v1) | Support phone, SMS, and email from the start. |
| 7.2 | Multi-tenant | Yes: Aventora settings (API key, domain, defaults, roles) are per workspace. Only Aventora base URL is in server .env. |
3. Current State Summary
| Layer | What exists | What’s missing |
|---|---|---|
| Twenty front | “Engage By Aventora” command on Person; start-engagement modal; Aventora section on Person (list of engagements, history, transcript) | — |
| Twenty server | REST start-engagement; Aventora config (defaults, roles, feature flag); AventoraEngagement entity; webhook receiver; accept & store history/transcript (jsonb) | — |
| Aventora-phone | POST /integration/start; CRM webhooks including Twenty (TWENTY_WEBHOOK_URL, TWENTY_WEBHOOK_SECRET); full payload (history + transcript as JSON arrays); pause/resume in call_history | Email channel if not already present |
4. Architecture Overview
[Twenty UI] → [Start modal: channel, mode, instruction, contact] → [Twenty Backend REST]
↑ ↑
| | POST /rest/aventora/start-engagement
| ↓
| | → [Aventora-phone /integration/start]
| | → store AventoraEngagement (engagementId, personId, workspaceId, status: accepted)
| |
| [Aventora-phone] → phone / SMS / email
| |
| | (on update/completion)
| ↓
| [Webhook POST] → [Twenty Backend POST /rest/aventora/webhook]
| | lookup by engagement_id → personId, workspaceId
| ↓
| [Upsert AventoraEngagement: status, outcome, history, transcript]
|
└── Person show page: “Aventora” section (list + latest expanded; Engage By Aventora in section + command menu)
- Start flow: User clicks "Engage By Aventora" → modal collects channel (phone/SMS/email), mode (Informational/Conversational/…), instruction, and contact if multiple. Twenty backend checks one active engagement per person; resolves person + workspace Aventora config + Aventora base URL (from server .env); calls Aventora-phone; stores AventoraEngagement row with engagement_id; returns engagement_id to client.
- Sync flow: Aventora-phone POSTs webhook with engagement_id and full payload. Twenty looks up AventoraEngagement by engagement_id to get workspace/person; upserts status, outcome, history, transcript (JSON). (See §5 Data Model.) “Aventora” (Data shown in Person Aventora section.)
5. Data Model in Twenty: AventoraEngagement
Final design (see §2 Design Decisions):
- Option A (preferred): New standard/custom object
AventoraEngagement(orEngagement) in Twenty:- Fields:
engagementId(external id),personId(relation to Person),status,outcome,summary,startedAt,completedAt,history(JSON or relation),transcript(JSON or relation),sourceRecordId,sourceOrgId,channel, etc. - Person record show page: add a section/tab “Aventora” that lists/summarizes
AventoraEngagementrecords for that person and shows history + transcript (e.g. expandable per engagement).
- Fields:
- Option B: Store engagement payload in a single JSON field on Person (e.g.
aventoraEngagements) and render a custom “Aventora” section from that. Simpler schema, less flexible for querying/filtering. - Option C (fallback): Use Notes with a specific type/label for Aventora and link to Person. You said you prefer an Aventora-specific section, so only if Option A/B are blocked.
Final design: AventoraEngagement object, hidden/internal for v1; history and transcript as JSON; one row per engagement.
6. Full Task Checklist
Phase 1: Start engagement (Twenty → Aventora)
1.1 Twenty server: start-engagement endpoint (REST)
- 1.1.1 Add REST endpoint
POST /rest/aventora/start-engagementwith body:personId,channel(phone | sms | email),type/mode (e.g. informational | conversational),instruction, contact identifier if person has multiple phones/emails. Auth from session/API token. - 1.1.2 Resolve current workspace id from auth context. Check feature flag: Aventora integration enabled for this workspace; else 403.
- 1.1.3 Load workspace Aventora config (from Aventora configuration screen / application variables):
AVENTORA_ASSIGNED_DOMAIN,AVENTORA_API_KEY, and defaults (channel, type/mode, instruction). Aventora base URL from server .env. Decrypt API key if stored encrypted. - 1.1.4 Load Person by id (workspace-scoped): get
phonesandemails. Validate: no phone and no email → 400; only email → channel email only; only phone → phone/sms only. (Legacy: supportphoneif still used; fromphones— first number or a designated “primary”); support legacyphoneif still used. - 1.1.5 One active engagement per person: if person has AventoraEngagement with status not completed/failed/cancelled → 400.
- 1.1.6 Call Aventora-phone
POST /integration/startwith:phone_number: from persondomain_name: from workspace Aventora configsource_record_id: personIdsource_org_id: workspaceIdsource_system:"twenty"source_object:"person"channel,type, etc. from request or defaults- Header: Aventora API key (e.g.
Authorization: Bearer <key>or header agreed with aventora-phone).
- 1.1.7 Map Aventora response to a start-engagement response (e.g.
{ engagementId, status, createdAt }). Return to client. - 1.1.8 Right after successful start, create an
AventoraEngagementrecord in Twenty withengagementId,personId,status: "accepted",startedAt, so the Aventora section can show “in progress” immediately.
1.2 Aventora-phone: auth and contract
- 1.2.1 Confirm how Twenty backend sends API key to Aventora (header name, format). Align with
require_call_management()in aventora-phone if needed. Done: Twenty sendsAuthorization: Bearer <api_key>; aventora-phone uses HTTPBearer (same). - 1.2.2 No change to
/integration/startrequest schema if current fields are enough; only ensuresource_system/source_objectare passed for future filtering. Done: Twenty passessource_system: "twenty",source_object: "person".
1.3 Twenty front: start-engagement modal and backend call
- 1.3.1 Point “Engage By Aventora” to the new Twenty backend endpoint (same-origin): e.g. set
REACT_APP_AVENTORA_START_ENDPOINTto relative path like/rest/aventora/start-engagementor use GraphQL mutation (no env for URL). Done: Default isREACT_APP_SERVER_BASE_URL/rest/aventora/start-engagement; optional env overrides. - 1.3.2 Send
personId(and optionally channel/type). Use existing auth (cookies/token) so backend has workspace + user. Done:credentials: 'include'; body includespersonId. - 1.3.3 (Optional) Before calling, validate that person has at least one phone; show warning or disable button if not.
- 1.3.4 On success, show snackbar with engagement id or “Engagement started”; on error, show backend error message. Done: Success shows engagement id when present; error shows backend message.
Phase 1 testing guide
Use this to verify Phase 1 end-to-end: Twenty → Aventora-phone start engagement.
Prerequisites
- Twenty repo built and runnable (front + server).
- Aventora-phone running and reachable from the machine where Twenty server runs (e.g. same host or known URL).
- PostgreSQL and Redis for Twenty (and any env from
packages/twenty-utils/setup-dev-env.shor your setup). - Migrations applied so
core.aventoraEngagementand Aventora application variables exist (migrations1774000000000-add-aventora-workspace-configurationand1775000000000-add-aventora-engagementor equivalent).
1. Configuration
1.1 Twenty server .env (e.g. packages/twenty-server/.env)
AVENTORA_BASE_URL— Base URL of Aventora-phone, no trailing slash. Examples:http://localhost:8010,http://localhost:8000, or your deployment URL. Twenty callsPOST {AVENTORA_BASE_URL}/integration/start.APP_SECRET— Must be set if Aventora API key is stored encrypted (needed for decryption).
1.2 Twenty workspace: Aventora application variables
Variables are per workspace, under the Twenty Standard application:
- In Twenty, go to Settings (gear) → Applications.
- Open the Twenty Standard application (or the one that has Aventora variables).
- Open the Settings (or Environment variables) tab.
- Set:
AVENTORA_ASSIGNED_DOMAIN— Domain name assigned to this workspace in Aventora (e.g. thedomain_nameyour Aventora-phone account uses).AVENTORA_API_KEY— API key that Aventora-phone accepts for call management (Bearer token). Must match the key configured in Aventora-phone for this domain/account.
If these are missing or empty, the start-engagement endpoint returns 403 with a message like "Aventora integration is not configured".
1.3 Aventora-phone
- Server running and
/integration/startavailable. - Authentication: Twenty sends
Authorization: Bearer <AVENTORA_API_KEY>. Ensure the key set in Twenty matches the one Aventora-phone expects (e.g. call management /require_call_management()). - Domain/account: The
domain_namesent by Twenty must be valid for the account tied to that API key.
2. Data to test with
- Workspace: Use a workspace where you set the Aventora variables above.
- Person: At least one Person with a phone number (primary or additional) so the default channel (phone) can be used; or an email if you will test the email channel.
- If the person has no phone and no email, start-engagement returns 400.
3. Run services
- Start Aventora-phone (e.g. on port 8010 or 8000). Note the base URL.
- Start Twenty server with
AVENTORA_BASE_URLset to that base URL (andAPP_SECRETif using encrypted API key). From repo root:npx nx run twenty-server:startornpx nx run twenty-server:start:fastif you already built. - Start Twenty front (e.g.
npx nx start twenty-front). Ensure it talks to the same Twenty server (e.g.REACT_APP_SERVER_BASE_URLor default).
4. Test flow: start engagement from Twenty
- Log in to Twenty and switch to the workspace where Aventora is configured.
- Open People and click a person that has at least one phone (or email).
- You should be on the Person show page (single record view).
- Open the record command menu (e.g. "…" or command palette for the record) and choose "Engage By Aventora".
- The app sends
POST /rest/aventora/start-engagementwith{ personId }and session auth (credentials: 'include').
Expected (success): A success snackbar (e.g. "Engagement started" or "Engagement started (<engagement_id>)"). In Aventora-phone, an engagement/call is created for that contact. In Twenty, an AventoraEngagement row exists for that person with the returned engagementId and status (e.g. accepted).
Expected (failure):
- 403 "Aventora integration is not configured" → Check workspace application variables and
AVENTORA_BASE_URL. - 403 "Aventora base URL is not configured" → Set
AVENTORA_BASE_URLin server.env. - 400 "Person has no phone or email" → Use a person with at least one phone or email.
- 400 "Person already has an active engagement" → One active engagement per person. Use the "Cancel Aventora Engagement" command on the Person (command menu) to cancel it, or wait for the Aventora tab (Phase 3) to list engagements and complete/cancel from there.
- 4xx/5xx from Aventora-phone (e.g. 401/403) → Check API key and domain in Aventora-phone; ensure Bearer token and
domain_nameare correct.
5. Optional: verify with REST directly
- Endpoint:
POST {TWENTY_SERVER_URL}/rest/aventora/start-engagement - Headers:
Content-Type: application/jsonand session cookie (or valid auth that sets workspace context). - Body:
{ "personId": "<uuid-of-person>" } - Same success/error behaviour as in the UI; response body includes
engagementId,status,createdAton success, or errormessageon failure.
6. One active engagement per person
- Start an engagement for a person and leave it in progress (not completed in Aventora).
- Trigger "Engage By Aventora" again for the same person.
- Expected: 400 with a message that the person already has an active engagement (no second engagement until the first is done/cancelled).
Phase 2: Aventora → Twenty data (webhook or poll)
2.1 Aventora-phone: Twenty webhook
- 2.1.1 Add
TWENTY_WEBHOOK_URLandTWENTY_WEBHOOK_SECRETto CRM webhook dispatcher (aventora-phoneservices/crm_webhooks/dispatcher.py), same pattern as Salesforce/Dynamics/HubSpot/Zoho. - 2.1.2 When engagement status changes, notify all CRMs (including Twenty): on engagement start (
notify_all_crm_webhooks_for_engagement), on call pause/resume/cancel and on call complete (schedule_salesforce_webhook_for_call→notify_all_crm_webhooks_for_call). Same payload (status, outcome, summary, history, transcript, etc.) andX-Aventora-Webhook-Secretif configured. - 2.1.3 Document in aventora-phone docs (e.g.
docs/CRM-Integration-Webhooks.md): env vars, payload shape, that Twenty is supported.
2.2 Twenty server: webhook receiver
- 2.2.1 Add REST endpoint for Aventora webhook, e.g.
POST /rest/aventora/webhook. - 2.2.2 Verify
X-Aventora-Webhook-Secretagainst secret (per workspace or global). Reject 401 if invalid. - 2.2.3 Parse payload:
engagement_id,status,outcome,summary,started_at,completed_at,history,transcript. - 2.2.4 Lookup by engagement_id: Find AventoraEngagement by
engagement_id(stored at start). From it get personId and workspaceId. No reliance on source_org_id/source_record_id in payload for lookup. - 2.2.5 Upsert that AventoraEngagement: update status, outcome, summary, timestamps, history (JSON), transcript (JSON).
- 2.2.6 Idempotency: same engagement_id + same/later completed_at/sent_at → merge without duplicating.
Alternative to webhook: Twenty backend could poll Aventora GET /integration/engagement/{id} for in-progress engagements (e.g. job that runs every N minutes). Checklist above assumes webhook; add a separate “Polling” sub-phase if you prefer polling instead.
Phase 2 verification: how to confirm the webhook is working
There is no Aventora tab in the Twenty UI yet (Phase 3.2). To verify Phase 2 (Aventora → Twenty webhook) is working:
-
Prerequisites
- Aventora-phone:
TWENTY_WEBHOOK_URLand (optionally)TWENTY_WEBHOOK_SECRETset; Twenty server reachable from Aventora-phone. - Twenty server: migration applied so
core.aventoraEngagementhasoutcome,summary,completedAt,history,transcript,webhookSentAt. If using secret,AVENTORA_WEBHOOK_SECRETset and equal toTWENTY_WEBHOOK_SECRET.
- Aventora-phone:
-
Run a full flow
- In Twenty, start an engagement for a person (Phase 1). Note the person and (if shown) the engagement id.
- In Aventora-phone, any status change (engagement start, pause, resume, cancel, or call completion) triggers a POST to
TWENTY_WEBHOOK_URL.
-
Check Twenty server logs
- When the webhook is received you should see:
Aventora webhook received for engagement_id=<id>. - When the engagement row is updated you should see:
Aventora engagement updated: engagement_id=<id>. - If the engagement was not found in Twenty (e.g. started from another CRM) you'll see a debug log:
Aventora webhook no-op: engagement_id=... (not found or older sent_at).
- When the webhook is received you should see:
-
Check Aventora-phone logs
- On success:
CRM webhook twenty notified for engagement <id>(at debug level). - On failure:
CRM webhook twenty POST <url> returned <status> for engagement <id>: <message>.
- On success:
-
Check the database
- Query Twenty's PostgreSQL:
SELECT "engagementId", status, outcome, summary, "completedAt", "webhookSentAt" FROM core."aventoraEngagement" ORDER BY "startedAt" DESC LIMIT 5; - After the webhook runs, the row for that engagement should have
status,outcome,summary,completedAt, andwebhookSentAtfilled (and optionallyhistory,transcript). Before the webhook, onlyengagementId,personId,workspaceId,status(e.g. accepted), andstartedAtare set.
- Query Twenty's PostgreSQL:
-
Optional: send a test webhook with curl
- Get an
engagement_idfrom a row incore.aventoraEngagement(one you created by starting from Twenty). curl -X POST https://<your-twenty-server>/rest/aventora/webhook -H "Content-Type: application/json" -H "X-Aventora-Webhook-Secret: <AVENTORA_WEBHOOK_SECRET>" -d '{"engagement_id":"<id>","status":"completed","outcome":"Done","summary":"Test","sent_at":"2025-01-15T12:00:00Z"}'- Check Twenty logs for "Aventora engagement updated" and the DB for updated columns.
- Get an
Phase 2 troubleshooting: rows stay with only engagementId and status
If core.aventoraEngagement rows never get outcome, summary, completedAt, or webhookSentAt filled, the webhook is either not being sent by Aventora-phone or not reaching Twenty. Check in this order:
-
When webhooks are sent: Aventora-phone sends the webhook on any status change: engagement start (accepted), call paused, call resumed, call cancelled, and call completed. So you should see a webhook soon after starting an engagement and again on pause/resume/cancel/complete. If you see no webhooks at all, check 2–5 below.
-
TWENTY_WEBHOOK_URLmust be set in Aventora-phone. In Aventora-phone’s.env, setTWENTY_WEBHOOK_URLto the full URL of Twenty’s webhook endpoint (e.g.https://your-twenty-host/rest/aventora/webhook). If this is unset, Twenty is not in the CRM webhook targets and no POST is sent. -
TWENTY_WEBHOOK_URLmust point to the Twenty server. The webhook is served by the Twenty server (NestJS), so use the same host and port as your Twenty server (e.g. the same asTWENTY_BASE_URL). For example, if Twenty server runs onhttp://localhost:3012, useTWENTY_WEBHOOK_URL=http://localhost:3012/rest/aventora/webhook. Using a different port (e.g. 8012 when the server is on 3012) will cause "All connection attempts failed" because nothing is listening on that port. -
Aventora-phone must be able to reach that URL. If Twenty runs on
localhost, Aventora-phone (e.g. in Docker or on another machine) cannot usehttp://localhost:3000/.... Use a reachable URL: same machine →http://host.docker.internal:3000/...or your machine’s IP; or expose Twenty via ngrok/tunnel and setTWENTY_WEBHOOK_URLto the tunnel URL. -
Prove Twenty’s webhook and DB update work with curl. Use an existing row’s
engagementId(e.g.eng_244112f77931420781fd3abb). If you use a secret, set the header; if you don’t use a secret, omit the header. Example (no secret):
curl -X POST http://localhost:3000/rest/aventora/webhook -H "Content-Type: application/json" -d "{\"engagement_id\":\"eng_244112f77931420781fd3abb\",\"status\":\"completed\",\"outcome\":\"Done\",\"summary\":\"Test\",\"sent_at\":\"2025-03-14T12:00:00Z\"}"
Then query the DB again: that row should now havestatus,outcome,summary,completedAt,webhookSentAtset. If it does, Phase 2 on Twenty is working and the issue is that Aventora-phone is not sending (see 1–4). -
Twenty migration for webhook fields. Ensure migration
1776000000000-add-aventora-engagement-webhook-fieldshas been run so the table has columnsoutcome,summary,completedAt,history,transcript,webhookSentAt. Without these, updates would fail.
Phase 3: Aventora object and Person section in Twenty
3.1 Twenty server: AventoraEngagement object
- 3.1.1 Define new object (standard or custom)
AventoraEngagement(orEngagement) with fields, e.g.:engagementId(text, unique per workspace)personId(relation → Person)status,outcome,summary(text)startedAt,completedAt(date/datetime)history(JSON or structured; or separate table if you prefer normalized)transcript(JSON or structured)channel,sourceRecordId,sourceOrgId(for reference)
- 3.1.2 Create migration/metadata so the object exists when Aventora integration is enabled (feature flag) (or only when Aventora app is “installed” if you use an app model).
- 3.1.3 Expose create/update (for start + webhook) and read by personId (for UI).
3.2 Twenty front: Aventora section on Person
Complete / Cancel engagements: The Aventora tab is the intended place to list engagements and provide Complete (mark as done) and Cancel actions for active engagements, so users can clear the block and start a new one. Optionally support delete for old engagements. Until this section exists, users can use the "Cancel Aventora Engagement" command on the Person (command menu) or the REST POST /rest/aventora/cancel-engagement endpoint.
- 3.2.1 Add an “Aventora” section (or tab) to the Person record show page. Prefer a dedicated section in the main layout (e.g. below or beside Notes/Timeline) so it’s clearly “Aventora” and not mixed with Notes.
- 3.2.2 In that section, query AventoraEngagements for current
personId(e.g. GraphQL or REST), ordered bystartedAtdesc. - 3.2.3 For each engagement, show: status, outcome, summary, started/completed time, and expandable history (attempts/events) and transcript (conversation messages).
- 3.2.4 (Optional) Show a compact “Last engagement” summary in the section header; “Engage By Aventora” button can stay in the command menu or be duplicated in this section.
- 3.2.5 Handle empty state: “No Aventora engagements yet” and CTA to start. If engagement not found in Aventora (e.g. 404), show as Broken.
Phase 4: Configuration and docs
4.1 Twenty
- 4.1.1 Document in Twenty (or in this doc) that workspace must have AVENTORA_ASSIGNED_DOMAIN and AVENTORA_API_KEY set (Settings → Applications → [Workspace application] → Settings → Configuration).
- 4.1.2 If using webhook: document where to set Twenty webhook URL and secret (per workspace or global). Twenty backend must expose a URL that Aventora-phone can reach (e.g. public or tunnel in dev).
4.2 Aventora-phone
- 4.2.1 Document
TWENTY_WEBHOOK_URLandTWENTY_WEBHOOK_SECRETindocs/CRM-Integration-Webhooks.md(or equivalent).
4.3 This plan
- 4.3.1 After implementation, update this doc with “Implemented” sections and any deviations (e.g. object name, polling instead of webhook).
7. Implementation Order (suggested)
- Phase 1 (start engagement): 1.1 → 1.2 → 1.3 so that “Engage By Aventora” works end-to-end with Aventora-phone.
- Phase 3.1 (AventoraEngagement object) can be done in parallel or right after 1.1 so that 1.1.8 can create a “pending” engagement row.
- Phase 2 (webhook + receiver) so that when a call completes, Twenty gets updates.
- Phase 3.2 (Aventora section UI) so users see history and transcripts on the Person.
- Phase 4 (configuration and docs) throughout or at the end.
7b. File / Area Reference (for implementers)
| Area | Location (example) |
|---|---|
| Twenty start-engagement API | twenty/packages/twenty-server: new module e.g. modules/aventora or under engine |
| Twenty webhook receiver | Same; REST route e.g. POST /rest/aventora/webhook |
| Aventora application variables | twenty-server/.../aventora-application-variables.constant.ts; read via application variable service |
| Person phone | Person entity phones (and deprecated phone); workspace-scoped Person API |
| Engage By Aventora front | twenty-front/.../EngageByAventoraPersonSingleRecordCommand.tsx; config in PeopleCommandMenuItemsConfig.tsx |
| Aventora-phone webhook list | aventora-phone/services/crm_webhooks/dispatcher.py (CRM_TARGETS_ENV) |
| Aventora-phone integration API | aventora-phone/server/routers/integration_engagement.py; services/salesforce_integration/service.py |
8. Resolved Decisions (see §2)
All design questions have been resolved and captured in §2 Design Decisions (Final). Open Decisions have been removed.
9. Backlog (non-priority)
Webhook retries: Aventora retry every n seconds for m times, then way to restart/retry.→ Phase 5 (see below).
Phase 5: Webhook retries (Aventora-phone)
When a CRM webhook POST fails (5xx or connection/timeout), Aventora-phone retries so transient outages don’t lose updates. Client errors (4xx) are not retried.
5.1 Specification
- Retry when: HTTP 5xx, or connection/timeout/network error (no retry on 4xx).
- Policy: Up to m attempts (first attempt + m−1 retries), with n seconds delay between attempts (configurable via env).
- Behaviour: Same payload and headers on each attempt; log each attempt; after final failure, log clearly (no in-app “restart/retry” in v1).
5.2 Aventora-phone implementation
- 5.2.1 Add env vars:
CRM_WEBHOOK_RETRY_COUNT(default 3 = 1 initial + 2 retries),CRM_WEBHOOK_RETRY_DELAY_SECONDS(default 5). Applied to all CRM targets (Salesforce, Dynamics, HubSpot, Zoho, Twenty). - 5.2.2 In
services/crm_webhooks/dispatcher.py, for each target: on 5xx or exception, sleep then retry up to configured count; on 4xx do not retry. - 5.2.3 Log each attempt (attempt number, status or error); on final failure log at warning with engagement_id and target name.
- 5.2.4 Document in
docs/CRM-Integration-Webhooks.mdthe retry env vars and behaviour.
10. Aventora configuration screen (Twenty)
- 10.1 Extend Aventora configuration (where API key and domain are set) to include: default channel, default type/mode, default instruction, roles that can start and view engagements, feature flag (enable/disable Aventora for workspace), and optionally webhook URL + secret if webhook is configured per workspace. Server .env holds Aventora base URL only.
11. Deploying Twenty (Aventora build) to a VPS
To run the Aventora-branded Twenty CRM on a VPS with Docker:
- Build the image from the twenty repo (not the public
twentycrm/twentyimage); tag asaventora-twenty:latest. - Compose: Use
packages/twenty-docker/docker-compose.aventora.ymlandpackages/twenty-docker/.env.aventora.examplein the twenty repo. - Full steps: See the twenty repo
docs/deploy-twenty-vps.mdfor the build command, required.envvars (includingAVENTORA_BASE_URL,AVENTORA_WEBHOOK_SECRET), and SSL/reverse-proxy notes.
12. Person ownership (createdBy) and PERSON_WALL
When PERSON_WALL is enabled on the Twenty server, workspace members only see Person records they created (or that match their scope field), plus system contacts unless configured otherwise.
Hub → CRM ownership resolution
Before creating a person, Hub resolves the engagement initiator via services/crm_sync/person_ownership_context.py:
- Load
crm_workspace_idfrom domainaccount_settings - Resolve initiator
external_user_id(and email from domain-chatbot users, oruser_phone_numberfor phone lookup) POST {CRM_API_URL}/auth/provision/person-ownership-contextwithCRM_PROVISIONING_SECRET- Cache result ~15 minutes per
(domain, user, phone, email)
Hub env: CRM_API_URL, CRM_PROVISIONING_SECRET (same as domain-chatbot). If missing on Hub, Hub falls back to domain-chatbot POST /stats/domains/{domain}/crm/person-ownership-context (domain API key). If both fail, Hub keeps legacy Aventora attribution.
Twenty server env
| Variable | Default | Purpose |
|---|---|---|
PERSON_WALL | false | Enable member-scoped Person reads |
PERSON_WALL_EXCLUDE_SYSTEM_RECORDS | false | When true, hide createdBy.source = SYSTEM rows from scoped members |
PERSON_WALL_SYSTEM_ACTOR_NAME | Aventora | createdBy.name for API/integration creates without a workspace member |
PERSON_WALL_WORKSPACE_ID | (optional) | Limit wall to one workspace |
After enabling, run once per deployment:
npx nx run twenty-server:command workspace:backfill-person-created-by
Hub → Twenty REST create
TwentyClient.create_person and resolve_or_create_contact accept optional ownership_context (PersonOwnershipContext from person_ownership_context.py) or legacy created_by_workspace_member_id. When set, the REST body includes:
{
"createdBy": { "workspaceMemberId": "<uuid>" },
"tipsAgentId": 42
}
The scope field name/value (e.g. tipsAgentId) is included when CRM returns them from person-ownership-context (TIPS and other PERSON_WALL_FIELD_NAME deployments).
When an existing person is found with Aventora / API attribution, Hub PATCHes createdBy and the scope field when ownership context is available.
When omitted, Twenty assigns SYSTEM / Aventora attribution (API key auth).
With PERSON_WALL_EXCLUDE_SYSTEM_RECORDS=false, all members also see contacts whose createdBy.source is SYSTEM, API, or APPLICATION (Hub / integration creates). With true, only contacts they created.
Debugging PERSON_WALL
Set PERSON_WALL_DEBUG=true on the Twenty server, recreate the container, log in (including via aventora-admin CRM SSO), open People, then grep logs:
docker compose logs server 2>&1 | grep PersonWall
Look for reason values such as filter_skipped_read_all_role, filter_skipped_no_role_id, scope_computed (workspace id mismatch), or filter_applied_created_by.
End of Twenty CRM ↔ Aventora integration plan. Design finalized; implement step by step and tick off checklist items.