Manage standard and custom Twenty records
Use this guide to manage one standard or custom object through a current schema snapshot, bounded record discovery, duplicate review, one governed mutation, and a provider read that verifies the intended result.
The connector applies the same outer object grammar to standard and custom objects. Twenty owns which objects and fields exist in the configured workspace and which fields are writable at the observed provider revision.
Prerequisites
Require:
- an admitted and healthy Twenty host for the intended tenant and workspace;
- a caller authorized to discover and invoke the selected capabilities;
- a trusted tenant approver for every mutation;
- an owner for the application’s object and field allowlist;
- a durable store for schema, Action, approval, key, and provider evidence;
- a data-handling policy for confidential and personal record content.
Set only AIP client coordinates:
export AIP_URL='https://aip.example.com'
export AIP_TOKEN_FILE='/run/secrets/aip-native-token'
export OBJECT='companies'
The operator owns the provider origin, workspace UUID, and API token. Do not add them to record input.
1. Confirm the admitted surface
Discover the capabilities needed for the intended path. A create path normally uses these operations:
aipctl \
--native-bearer-token-file "$AIP_TOKEN_FILE" \
capability list "$AIP_URL" \
--capability-id cap:twenty:openapi.core \
--limit 1
aipctl \
--native-bearer-token-file "$AIP_TOKEN_FILE" \
capability list "$AIP_URL" \
--capability-id cap:twenty:record.list \
--limit 1
aipctl \
--native-bearer-token-file "$AIP_TOKEN_FILE" \
capability list "$AIP_URL" \
--capability-id cap:twenty:record.create \
--limit 1
Record the catalog revision and each contract and schema digest. Stop when an operation is absent; a syntactically valid ID cannot bypass admission.
For updates or lifecycle changes, discover that exact capability before constructing the Action.
2. Capture the current workspace schema
Read the core OpenAPI document with an empty input:
export SCHEMA_ACTION_ID='act_twenty_records_schema_001'
aipctl \
--native-bearer-token-file "$AIP_TOKEN_FILE" \
action call "$AIP_URL" \
cap:twenty:openapi.core \
--action-id "$SCHEMA_ACTION_ID" \
--mode sync \
--input '{}'
Retain the body under tenant-scoped access and calculate its digest outside the connector. Record the workspace, credential revision, timestamp, provider request ID, and connector artifact identity without retaining secret bytes.
Confirm that OBJECT exists and record:
- provider object name and label;
- required and writable fields;
- field types and relationship targets;
- unique and duplicate-sensitive fields;
- fields that trigger automation or notification;
- read and write permissions of the fixed provider credential;
- fields excluded from application input, output, logs, and evidence.
The document is an observation, not a compatibility guarantee. Stop when the observed object contract differs from the application’s reviewed contract.
3. Freeze an application object contract
Create a versioned application record that binds:
{
"workspace_identity": "deployment-owned",
"object": "companies",
"openapi_digest": "sha256-of-observed-document",
"allowed_read_fields": ["id", "name"],
"allowed_write_fields": ["name"],
"duplicate_fields": ["name"],
"maximum_bulk_records": 1
}
This is application policy, not connector input. Keep its revision with every governed mutation.
For a custom object, derive every listed field from the observed workspace schema. Do not copy a standard-object example into a custom object without reviewing types, required values, and relationships.
4. Read a bounded existing set
Write record-list.json using the exact object:
{
"object": "companies",
"query": {
"limit": 25,
"filter": "name[like]:AIP%",
"depth": 0
}
}
Submit the read:
export LIST_ACTION_ID='act_twenty_records_list_001'
aipctl \
--native-bearer-token-file "$AIP_TOKEN_FILE" \
action call "$AIP_URL" \
cap:twenty:record.list \
--action-id "$LIST_ACTION_ID" \
--mode sync \
--input @record-list.json
Validate that returned fields, object identity, pagination, and result size match the frozen application contract. Apply output minimization before storing or displaying provider records.
5. Review duplicate candidates
Before create, write one candidate body under data:
{
"object": "companies",
"body": {
"data": [
{
"name": "AIP Managed Company"
}
]
},
"query": {
"depth": 0
}
}
Invoke cap:twenty:record.find_duplicates with a new read Action. Review the
provider candidates against the application’s duplicate fields and current
business ownership.
Duplicate detection provides candidates. It does not select a survivor, authorize merge, or prove that no duplicate exists.
Choose one path:
| Observation | Next intent |
|---|---|
| No acceptable match | Create one deterministic record |
| One record is the intended entity | Update that exact ID when a change is needed |
| Several records need consolidation | Stop and prepare a separately approved merge |
| Schema or ownership is unclear | Stop and refresh the object contract |
6. Freeze one mutation
For create, write the provider body once:
{
"object": "companies",
"body": {
"name": "AIP Managed Company"
},
"query": {
"depth": 0
}
}
For update, use the validated provider ID and only approved writable fields:
{
"object": "companies",
"id": "0190c42f-2d5a-7000-8000-000000000060",
"body": {
"name": "AIP Managed Company Updated"
},
"query": {
"depth": 0
}
}
Freeze capability, input bytes, schema digest, object-policy revision, target ID, expected prior values, and expected result. A semantic change requires a new intent.
7. Submit one governed mutation
Reserve stable identities for the selected path:
export MUTATION_CAPABILITY='cap:twenty:record.create'
export MUTATION_INPUT='record-create.json'
export MUTATION_ACTION_ID='act_twenty_records_create_001'
export MUTATION_KEY='twenty-records-create-v1'
Submit once:
aipctl \
--native-bearer-token-file "$AIP_TOKEN_FILE" \
action call "$AIP_URL" \
"$MUTATION_CAPABILITY" \
--action-id "$MUTATION_ACTION_ID" \
--idempotency-key "$MUTATION_KEY" \
--mode sync \
--input @"$MUTATION_INPUT"
Without admitted authorization, the Action enters requires_human. The trusted
approver reviews the frozen input, schema and policy identities, duplicate
evidence, target, consequences, and required reason.
Approval resumes the same queued Action. Do not submit a replacement Action or change the key while the original remains pending.
Create forces provider upsert and derives a missing record ID from workspace, key, and index zero. Update preserves the supplied target ID. Both advertise retry safety only with the original key and unchanged canonical input.
8. Read the durable mutation result
Read the original Action:
aipctl \
--native-bearer-token-file "$AIP_TOKEN_FILE" \
action status "$AIP_URL" \
"$MUTATION_ACTION_ID" \
--include-result \
--include-receipts \
--wait-ms 30000
Proceed only after terminal completed. Retain provider HTTP status, provider
request ID when present, decoded body, approval, and receipts.
A completed response does not prove that provider automation, webhooks, notifications, indexes, or integrations have completed.
9. Verify the provider record
Extract a validated provider record ID from the result. Never derive it from an Action ID or assume the application name is unique.
Write record-get.json:
{
"object": "companies",
"id": "0190c42f-2d5a-7000-8000-000000000060",
"query": {
"depth": 0
}
}
Invoke cap:twenty:record.get under a new read Action. Compare only fields
owned by the frozen application contract and retain the verification timestamp
and provider request identity.
If the result differs, do not overwrite it automatically. Classify schema drift, provider validation, automation, concurrency, wrong target, or uncertain prior execution before preparing another mutation.
10. Choose a separate lifecycle intent
Use a new Action, key, approval, and current preflight for every lifecycle change.
| Intent | Capability | Boundary |
|---|---|---|
| Hide one recoverable record | record.soft_delete |
Restore depends on provider-retained state |
| Restore one soft-deleted record | record.restore |
Uses a filtered collection route at this revision |
| Permanently remove one record | record.destroy |
Critical; no connector rollback |
| Consolidate duplicates | record.merge |
Two through nine unique IDs; provider chooses merge semantics |
| Change a bounded set | A *_many operation |
Explicit non-empty filter and target evidence required |
Do not treat soft delete and restore as one transaction. Do not use permanent destroy as duplicate cleanup without retention and dependency review.
Recover without duplicating work
| Observation | Response |
|---|---|
Action is requires_human |
Complete the existing approval workflow |
| Action remains pending | Read the same Action; do not replace it |
| Idempotency collision | Stop; the key owns different canonical input |
| Definitive provider rejection | Refresh schema and state before a new intent |
| Retryable create or single-update failure | Reuse only the original Action and key according to runtime policy |
| Uncertain mutation outcome | Preserve all identities and inspect provider state before another write |
| Provider record cannot be identified | Stop; do not guess an ID or repeat create |
The connector exposes no reconciliation capability. Bounded provider reads and retained evidence must establish current state before a separately authorized recovery mutation.
Retain the management evidence
Keep:
- catalog revision and selected capability digests;
- connector artifact, provider mapping, tenant, workspace, and credential revisions;
- observed OpenAPI body digest and application object-policy revision;
- bounded list and duplicate Actions and their minimized results;
- frozen mutation input and expected prior and resulting values;
- Action ID, idempotency key, approval, policy hash, result, and receipts;
- provider request and validated record IDs;
- post-mutation read and comparison;
- any lifecycle or recovery Action as a separate evidence chain.
This evidence demonstrates one controlled record workflow. It does not by itself establish live-provider qualification or general compatibility for every standard or custom object.