fn ping()
Probe the Twenty instance: an authed GET /rest/people?limit=1. A 2xx proves it is reachable AND the token authenticates. Snapshots a `capability` resource.
fn introspectSchema()
Read live object/field metadata (GET /rest/metadata/objects): list objects, discover the Opportunity stage enum, and report whether the required leadId/isEmergency custom fields exist on Person/Opportunity/Note. Snapshots a `schema` resource. Run this before trusting leadId filters.
fn ensureLeadFields()
Idempotently provision the custom fields the lead sink depends on: leadId (TEXT) on Person/Opportunity/Note and isEmergency (BOOLEAN) on Person/Opportunity, via POST /rest/metadata/fields. Already-present fields are skipped. Confirm-gated (mutates workspace metadata). Snapshots a `fieldsEnsured` resource.
fn ensureStageOption()
Idempotently ensure a SELECT option exists on an allowlisted picklist field (default opportunity.stage), so an Opportunity can be set to a stage the workspace didn't ship with (e.g. CLOSED). Reads the field's FULL option set and appends the new option, preserving every OTHER existing option (id/label/color/position) verbatim — never a drop or reorder. If the value already exists with a differing label/color it is, by default (reconcile), updated IN PLACE (id/position preserved) since swamp owns
fn findPersonByLeadId(leadId: string)
Look up a Person by the immutable leadId marker (GET /rest/people?filter=leadId[eq]:<id>). Snapshots a `record` reference if found; writes nothing if not.
| Argument | Type | Required | Description |
|---|
| leadId | string | yes | Opaque upstream lead id |
fn findOpportunityByLeadId(leadId: string)
Look up an Opportunity by the immutable leadId marker (GET /rest/opportunities?filter=leadId[eq]:<id>) — the primary idempotency check. Snapshots a `record` reference if found.
| Argument | Type | Required | Description |
|---|
| leadId | string | yes | Opaque upstream lead id |
fn findPerson()
Look up a Person by primary email OR leadId (exactly one). Read-only existence check for reconcile/dedup: records a `personRef` on both hit (found:true + id) and miss (found:false), so callers can tell 'looked, not there' from 'never looked'. Throws on an ambiguous (>1) email match. No writes.
fn getPersonById(id: string)
Fetch a Person by UUID (GET /rest/people/{id}); records a `personRef` with found:false on a 404. Read-only — lets the reconcile report walk from an opportunity's pointOfContactId back to a contact.
| Argument | Type | Required | Description |
|---|
| id | string | yes | Person UUID |
fn findCompany()
Look up a Company by domain OR exact name (exactly one). Records a `companyRef` on hit and miss. Throws on an ambiguous (>1) match; the name path returns found:false gracefully if the name field is not filterable on the instance. No writes.
fn getCompanyById(id: string)
Fetch a Company by UUID (GET /rest/companies/{id}); records a `companyRef` with found:false on a 404. Read-only — lets the reconcile report resolve an opportunity's companyId.
| Argument | Type | Required | Description |
|---|
| id | string | yes | Company UUID |
fn getOpportunity()
Fetch one Opportunity by leadId OR id (exactly one). Records an `opportunityRef` snapshot carrying the reconcile-critical fields (id, leadId, name, stage, amount in whole units, currencyCode, closeDate, companyId, pointOfContactId) plus the segmentation SELECTs (lineOfBusiness, sourceChannel) and isEmergency when set — so a written custom-field value is read-back verifiable; found:false + no fields on a miss. No writes.
fn listOpportunities(companyId?: string, stage?: string)
Fan-out read (repo rule 6): list Opportunities filtered by companyId and/or stage (both optional; neither => all, capped). Composes filters with AND, pages through Twenty's cursor pagination up to `limit` (hard-capped at 500), dedups by id, and records an `opportunityList` snapshot of compact views (id/leadId/name/stage/amount/currency/closeDate/companyId + segmentation lineOfBusiness/sourceChannel + isEmergency when set) + a `truncated` flag. No writes, no per-id loop.
| Argument | Type | Required | Description |
|---|
| companyId? | string | no | Filter: company UUID |
| stage? | string | no | Filter: opportunity stage (e.g. PROPOSAL) |
fn listPeople(companyId?: string, leadId?: string, includeEmergency: boolean, startingAfter?: string)
Fan-out read (repo rule 6): list People, optionally filtered by companyId and/or leadId (both optional; neither => all, capped-and-continued). Emergency-restricted rows are EXCLUDED by default via a NULL-safe clause (AR-5+SR-3). Composes filters with AND, sends order_by=createdAt,id, pages Twenty's cursor pagination up to a per-call cap (500), dedups by id, and records a `peopleList` page snapshot (join keys only — NO name/email/phone/jobTitle) with continuation + completeness fields. No writes,
| Argument | Type | Required | Description |
|---|
| companyId? | string | no | Filter: company UUID (Twenty's flat relation FK) |
| leadId? | string | no | Filter: immutable lead marker (TEXT custom field) |
| includeEmergency | boolean | yes | When false (default) excludes emergency-restricted rows NULL-safely (false OR unset); true opts them in |
| startingAfter? | string | no | Continuation cursor: a prior call's nextCursor, to resume paging past the per-call cap |
fn listCompanies(domain?: string, name?: string, startingAfter?: string)
Fan-out read (repo rule 6): list Companies, optionally filtered by domain (domainName.primaryLinkUrl) and/or name (both optional; neither => all, capped-and-continued). Composes filters with AND, sends order_by=createdAt,id, pages up to a per-call cap (500), dedups by id, and records a `companyList` page snapshot (id/name/domain) with continuation + completeness fields. No writes, no per-id loop.
| Argument | Type | Required | Description |
|---|
| domain? | string | no | Filter: corporate domain (normalized to a bare host, e.g. acme.com) |
| name? | string | no | Filter: exact company name (filter-safe; reserved chars bounded-reject) |
| startingAfter? | string | no | Continuation cursor: a prior call's nextCursor |
fn listNotes(leadId?: string, startingAfter?: string)
Fan-out read (repo rule 6): list Notes, optionally filtered by leadId (optional; absent => ALL notes, capped-and-continued). Sends order_by=createdAt,id, pages up to a per-call cap (500), dedups by id, and records a `noteList` page snapshot with continuation + completeness fields. Note body (bodyV2.markdown) is NEVER included; title is emitted ONLY on the machine 'Inbound lead ' pattern (SR-1). No emergency filtering: this extension does not provision isEmergency on Note (only Person/Opportunity
| Argument | Type | Required | Description |
|---|
| leadId? | string | no | Filter: immutable lead marker (TEXT custom field on Note) |
| startingAfter? | string | no | Continuation cursor: a prior call's nextCursor |
fn push_leads()
THE fan-out lead sink (repo rule 6). Ingest a batch of contact-form leads into Twenty in one execution: select status=='new', FIFO by received_at, capped at maxBatch. Per lead, isolated in try/catch: validate + sanitize every field FIRST (bad lead => failed, no writes); reuse but NEVER structurally mutate an existing Person; create/link a Company only for business leads on a real corporate domain; create exactly one Opportunity keyed on leadId (skip if it exists); ALWAYS ensure the per-lead mess
fn upsertOpportunity(name: string)
Generalized, idempotent Opportunity upsert keyed on leadId — the create/update path with the full field set (name, amount, stage, closeDate, company, point of contact, and the lineOfBusiness/sourceChannel segmentation SELECTs) that push_leads' bare createOpportunity omits. Finds any existing Opportunity by leadId: hit => PATCH the provided fields; miss => create. Optionally finds-or-creates and links a Company (by domain, else by exact name) and a point-of-contact Person (by email), and attaches
| Argument | Type | Required | Description |
|---|
| name | string | yes | Opportunity name |
fn upsertPerson(firstName?: string, lastName?: string, phone?: string, jobTitle?: string, city?: string)
Idempotent, confirm-gated curated-contact writer keyed on primaryEmail. Find-or-create a Person and set name/phone/jobTitle/city and an optional company link: hit => PATCH only the provided fields (a re-run never clobbers an unset field, and a partial name never nulls the other subfield); miss => create. Unlike push_leads/createPerson this NEVER stamps a leadId (curated contacts stay out of the lead namespace). Company is deduped by domain then exact name and created only when a domain is suppli
| Argument | Type | Required | Description |
|---|
| firstName? | string | no | First name |
| lastName? | string | no | Last name |
| phone? | string | no | Phone (normalized toward E.164) |
| jobTitle? | string | no | Job title |
| city? | string | no | City |
fn ensureField(label?: string)
Idempotently provision ONE custom field on an object via POST /rest/metadata/fields — the generalized foundation ensureLeadFields is now a thin wrapper over. Supports TEXT / BOOLEAN / NUMBER / DATE_TIME / SELECT. Non-destructive to structure: an absent field is created; a present scalar field is a no-op (a differing type is reported, never mutated); a present SELECT gets NEW options APPENDED and, by default (reconcile), existing options whose label/color drifted RECONCILED in place (id/position
| Argument | Type | Required | Description |
|---|
| label? | string | no | Display label |
fn ensureOpportunitySegmentation()
Fan-out (repo rule 6): idempotently provision the two Opportunity segmentation SELECT fields — Line of Business (Consulting / Hosting / Games) and Source Channel (Direct / Referral / Consulting hand-off) — through the shared append-only ensureField path in ONE execution (single GET, one lock). Analytics only, not a pipeline gate. Re-run is a clean no-op when the live options already match the spec; a field that exists with extra options keeps them, and by default (reconcile) an option whose labe
fn ensureObject()
Idempotently provision ONE custom OBJECT (object metadata type) via POST /rest/metadata/objects — the schema-provisioning foundation relation/field work builds on. Non-destructive: an object whose nameSingular/namePlural already exists is a no-op (reported as `present`, never mutated); a missing object is created. nameSingular/namePlural are camelCase identifiers and must differ; labels default to the title-cased names. dryRun:true (the DEFAULT) validates + plans (planned-create) and writes noth
fn ensureRelation()
Idempotently provision ONE custom RELATION between two existing objects. In Twenty a relation IS a field: this POSTs to /rest/metadata/fields with type RELATION plus a relationCreationPayload block, and the ONE call auto-creates BOTH sides (the reverse field on the target is server-minted from targetFieldLabel — never posted here). relationType is MANY_TO_ONE or ONE_TO_MANY only (no first-class MANY_TO_MANY). Non-destructive: a source field whose name already exists as a RELATION is a no-op (`pr
Resources
capability(infinite)— Reachability + auth probe snapshot
schema(infinite)— Introspection snapshot: objects, opportunity stage enum, required custom-field presence
fieldsEnsured(infinite)— Record of an ensureLeadFields run (created/present/failed)
record(infinite)— Reference to a single Person/Company/Opportunity/Note touched by a base method
pushRun(infinite)— Audit of one push_leads run: counts + per-lead results (no raw PII beyond leadId)
opportunityUpsert(infinite)— Result of an upsertOpportunity run: the action taken and the resolved opportunity/company/contact ids
personRef(infinite)— Existence-check snapshot from findPerson/getPersonById (found + join keys, no bulk PII)
companyRef(infinite)— Existence-check snapshot from findCompany/getCompanyById (found + id/domain/name)
opportunityRef(infinite)— Snapshot from getOpportunity: the reconcile-critical opportunity fields (id, leadId, name, stage, amount, ...)
opportunityList(infinite)— Snapshot from listOpportunities: a filtered, paginated set of opportunity views + truncation flag
peopleList(3d)— Snapshot from listPeople: a filtered, paginated, deduped page of compact person views (join keys only, no bulk PII) + continuation/completeness fields
companyList(3d)— Snapshot from listCompanies: a filtered, paginated, deduped page of compact company views (id/name/domain) + continuation/completeness fields
noteList(3d)— Snapshot from listNotes: a filtered, paginated, deduped page of compact note views (no body; title only on the machine 'Inbound lead ' pattern) + continuation/completeness fields
stageOption(infinite)— Result of an ensureStageOption run: the target picklist, the option, the action taken, and the full option set
fieldEnsured(infinite)— Result of an ensureField run: the field, the action taken (created/present/appended/planned), and any SELECT options added/present + non-mutating drift notes
personUpsert(infinite)— Result of an upsertPerson run: the action taken, which fields were set, and the company link (no raw PII)
objectEnsured(infinite)— Result of an ensureObject run: the custom object's identity, the action taken (created/present/planned-create), and the create payload
relationEnsured(infinite)— Result of an ensureRelation run: both objects' identities, the source/reverse field names, the relation settings/relation read-back, the action taken (created/present/planned-create/type-mismatch), and the create payload