Download all docs
data

Entity

The circle's web-of-interaction registry: one unified record for every party you maintain a relationship with — people and companies today, AIs, websites, groups, and chatrooms ahead — each carrying its identity, its reachable handles, and the consent that governs whether you may contact it.

Working with it

Selecting a Entity reveals its settings in the properties panel; it has no dedicated full-screen workbench.

How it appears

The same element type rendered as a definition, a circle instance, and a live workspace card.

En
type

Entity

Anything you maintain a record of and interact with — people, companies, AIs, websites, groups, chatrooms

dataatomdefinition

When to use / not

When to use

  • Keeping a circle-local address book / CRM of the people and companies you deal with, with their emails, phones, social profiles, messaging handles, and addresses on one record.
  • Gating outbound messaging on consent — record per-handle opt-in and a master do-not-contact switch, then have email / SMS / voice / DM send-gates check find_by_email or find_by_phone before they reach a recipient.
  • Deduplicating and looking up parties by a strong key — a Company's domain or org number, a Person's email or phone — across everything attached to them via search and the find_by_* verbs.
  • Holding GDPR-shaped evidence (Art. 7 proof-of-consent, Art. 21 objection reason + source + timestamp) alongside the contact it applies to.

When not to use

  • Modelling many-to-many relationships with their own properties — a Person's work history across several Companies, or who belongs to a Group — belongs in the dedicated relationship element, not as scalar fields here (entity only carries the cardinality-1 primary_company_id shortcut).
  • Storing records that aren't parties you interact with — freeform documents belong in document, tabular rows in sql, and arbitrary node/edge graphs in graph.
  • Reaching today's contacts or organizations data — those elements remain canonical at write time until the dual-write shim lands; entity is additive on first ship.

Topology

Created from the library and placed inside an app or circle. It is a top-level building block you compose with other elements.

Properties

No configurable properties.

Operations

  • activityGET
  • add_addressPOST
  • add_emailPOST
  • add_messagingPOST
  • add_phonePOST
  • add_socialPOST
  • attachmentsGET
  • batch_statsGET
  • composePOST
  • contextGET
  • createPOST
  • deletePOST
  • disablePOST
  • enablePOST
  • export_bundleGET
  • find_by_emailPOST
  • find_by_messagingPOST
  • find_by_phonePOST
  • find_by_socialPOST
  • getPOST
  • import_bundlePOST
  • intentionGET
  • listPOST
  • promotePOST
  • readmeGET
  • readme_updatePOST
  • remove-modifierPOST
  • remove_addressPOST
  • remove_emailPOST
  • remove_messagingPOST
  • remove_phonePOST
  • remove_socialPOST
  • restorePOST
  • schemaGET
  • searchPOST
  • set_do_not_contactPOST
  • set_email_consentPOST
  • set_phone_consentPOST
  • set_primary_companyPOST
  • sourceGET
  • source_branchesGET
  • source_fixturesPOST
  • source_promotePOST
  • source_repairPOST
  • source_statusGET
  • source_validatePOST
  • statsPOST
  • tagPOST
  • treeGET
  • untagPOST
  • updatePATCH
  • update_addressPOST
  • update_metaPATCH
  • upsertPOST
  • verify_emailPOST
  • verify_phonePOST
  • versionGET

Composition

Entity (entity)

Category: data | Form: | Symbol: En

Anything you maintain a record of and interact with — people, companies, AIs, websites, groups, chatrooms

The web-of-interaction registry. Each entity has a kind, an identity (name + handles), and zero or more relationships to other entities. Phase 2 of the redesign (see .triform/design/web-of-interaction/ 01-entities-and-collections.md) ships two kinds:

• Person — equivalent to today’s data/contacts row. Carries the full person-shaped property set (first/last names, title, pronouns, avatar, do_not_contact + GDPR Art. 21 evidence fields, owner, source, tags, custom, notes) plus five child groupings (emails, phones, socials, messaging handles, addresses) each with per-handle consent + verification flags.

• Company — equivalent to today’s data/organizations row. Carries the institution-shaped property set (legal_name, org_no, domain, industry, size, website, logo_url, description, owner, tags, custom, notes) plus four child groupings (emails, phones, socials, addresses). No messaging handles — companies aren’t reached via WhatsApp/Telegram at the org level. No per-handle consent — reception-desk addresses (info@, support@) are impersonal.

Phase 4 adds kinds AI, Website, Group, Chatroom. The kind set is open: storage stores kind as TEXT and the chemistry generator emits a kind registry from chemistry/elements/data/entity/.triform/kinds/*.yaml so adding a new kind is a YAML diff plus a portal variant component, no schema migration. A scalar shortcut primary_company_id on a Person row mirrors today’s contacts.organization_id — cardinality-1 with no edge properties. Cardinality-N relationships (Person works at multiple Companies with role + dates; Group includes Persons; Chatroom includes Persons + AIs) are stored in the data/relationship element shipping in Phase 6. The legacy data/contacts and data/organizations elements remain canonical at write time until a follow-up “shim PR” wires their ops to dual-write through this element. This element is fully additive on first ship — the per-circle entities table is created and backfilled from contacts+organizations (UUID-preserving), but writes to old endpoints stay on old tables until the shim lands. Backing per-circle schema (circle_{uuid}.entities plus its five child tables — entity_emails, entity_phones, entity_socials, entity_messaging, entity_addresses) is created by migration 0132 and re-applied to every circle via the standard _patch_circle_schema walker on first pod boot.

Guide

Anything you maintain a record of and interact with — people, companies, AIs, websites, groups, chatrooms

What It Does

Entity is the web-of-interaction registry — a per-circle, Postgres-backed data substrate that stores every person and organization a circle interacts with. Each entity row carries a kind, an identity (a required name plus optional handles), and zero or more child groupings: emails, phones, social profiles, messaging handles, and physical addresses.

The element ships two kinds. Person carries a person-shaped property set (first_name, last_name, title, pronouns, avatar_url, the four do_not_contact* GDPR Art. 21 fields, discovery_source, primary_company_id, plus owner, source, tags, custom, notes) and supports all five child types with per-handle consent on email/phone/messaging. Company carries an institution-shaped set (legal_name, org_no, domain, industry, size, website, logo_url, description, plus the shared fields) and supports four child types — no messaging — with no per-handle consent anywhere. The kind field routes per-kind property validation through a generated kind registry built from chemistry/elements/data/entity/.triform/kinds/*.yaml.

Other elements attach to an entity by reference rather than by wiring (runtime.wirable: false). Outbound send-gates (email, SMS, voice, DM) look a recipient up with the find_by_* read ops to decide whether an address, number, or handle is known to the circle; the do_not_contact master switch on a Person sits above per-handle consent and blocks every outbound channel when set. A scalar primary_company_id on a Person links it to a Company entity (cardinality-1, no edge properties).

Element Definition

PropertyValue
Typeentity
Categorydata
Formatom
SymbolEn
Iconhub / #7C3AED
Open modeworkbench (workbench-props)

Runtime

FieldValue
storage_backendpostgres
has_datatrue
wirablefalse
llm_roledata
llm_priority7

Kinds

The kind set is open — storage holds kind as text and the generator emits a kind registry from .triform/kinds/*.yaml. Two kinds ship today:

KindLabel (singular / plural)Child typesPer-handle consent
PersonContact / Contactsemail, phone, social, messaging, addressyes (email, phone, messaging)
CompanyOrganization / Organizationsemail, phone, social, addressno

Both kinds require only name. Dedup keys when no explicit id is given: Person matches on email / phone / messaging handle; Company matches on org_no / domain.

Storage

Backing per-circle schema (circle_{uuid}.entities plus the child tables entity_emails, entity_phones, entity_socials, entity_messaging, entity_addresses) is created by migration 0132 and re-applied to every circle via the standard _patch_circle_schema walker on first pod boot.

Observability

Counters/histograms emitted (labels in parentheses): entities_inserted_count (element_id, kind, actor_kind, source), entities_updated_count (element_id, kind, actor_kind), entities_deleted_count (element_id, kind, actor_kind), entities_query_count (element_id, kind), and the entities_query_latency_ms histogram (element_id, kind; buckets 5/25/100/250/1000/5000 ms).

Operations

All operations are POST to /api/{circle}/{slug}/ops/{op}. auth: read ops are read-only lookups; auth: write ops mutate.

Core entity

upsertPOST .../ops/upsert (write)

Create or update an entity with kind-aware property validation; merges child groupings by natural key. Idempotent on id (when provided) or the kind’s dedup keys. kind and name are required. Child arrays UPSERT their rows (existing rows matched on natural key merge, unnamed rows are preserved); tags union; custom JSON merges shallowly. Company-kind upserts that include consent_* fields on child rows reject with 400. Returns the full entity with all child groupings populated.

getPOST .../ops/get (read)

Fetch one entity with all child groupings (emails, phones, socials, messaging, addresses). Requires id.

listPOST .../ops/list (read)

Page through entities, optionally filtered by kind, primary_company_id, or tag (filters AND together). Defaults: limit 100, offset 0. Without filters, returns every entity newest-first. Returns { entities, total }.

searchPOST .../ops/search (read)

Free-text substring search across name + child handles (email, phone, domain, messaging, social). Requires query; optional kind filter; limit default 50. Returns { entities } with children populated.

deletePOST .../ops/delete (write)

Hard delete of the entity row and every child row (CASCADE) — GDPR right-to-erasure. Requires id. Cross-entity links (Person.primary_company_id) become NULL on the dependent side; deleting a Company does NOT delete its Persons.

tagPOST .../ops/tag (write)

Add a tag to an entity. Requires id, tag.

untagPOST .../ops/untag (write)

Remove a tag from an entity. Requires id, tag.

set_primary_companyPOST .../ops/set_primary_company (write)

Person only. Set or clear primary_company_id (scalar Person→Company shortcut). Pass primary_company_id to set; omit or pass null to clear. The target must exist with kind=Company. Requires id.

set_do_not_contactPOST .../ops/set_do_not_contact (write)

Person only. Set or clear the master do-not-contact switch; when true every send-gate refuses outbound regardless of per-handle consent. Records reason + source + timestamp for GDPR Art. 21 evidence. Rejects with 400 when the target has kind=Company. Requires id, value.

statsPOST .../ops/stats (read)

Aggregate counts. Optional kind filter. Returns total_entities, persons, companies, total_emails, total_phones, total_socials, total_messaging, total_addresses.

Emails

add_emailPOST .../ops/add_email (write)

Attach an email to an entity; idempotent on (entity_id, address); address lowercased on insert. Consent fields (consent_status, consent_source, consent_evidence) accepted only for kind=Person, rejected 400 for kind=Company. Requires entity_id, address.

remove_emailPOST .../ops/remove_email (write)

Detach an email address. Requires entity_id, address.

find_by_emailPOST .../ops/find_by_email (read)

Lookup an entity by email address — used by outbound send-gates. Matches across Person and Company. Requires address. Returns { entity, email }.

verify_emailPOST .../ops/verify_email (write)

Person only. Mark an email verified (double-opt-in confirmed); upgrades a prior soft_optin to double_optin. Rejects 400 for kind=Company. Requires entity_id, address.

set_email_consentPOST .../ops/set_email_consent (write)

Person only. Record consent state (consent_status / consent_source / consent_evidence / consent_at) for a specific address. Rejects 400 for kind=Company. Requires entity_id, address, consent_status.

Phones

add_phonePOST .../ops/add_phone (write)

Attach a phone number; normalised to E.164 on insert; idempotent on (entity_id, normalised number). Consent fields Person-only. Requires entity_id, number.

remove_phonePOST .../ops/remove_phone (write)

Detach a phone number. Requires entity_id, number.

find_by_phonePOST .../ops/find_by_phone (read)

Lookup an entity by phone number (E.164-normalised) — used by SMS / voice send-gates; spans Person and Company. Requires number. Returns { entity, phone }.

verify_phonePOST .../ops/verify_phone (write)

Person only. Mark a phone number verified (SMS/call confirmation). Requires entity_id, number.

set_phone_consentPOST .../ops/set_phone_consent (write)

Person only. Record consent state for a specific phone number. Requires entity_id, number, consent_status.

Socials

Public profile pointers — no consent — for both Person and Company.

add_socialPOST .../ops/add_social (write)

Attach a social profile; idempotent on (entity_id, platform, handle_or_url); platform is an open string. Requires entity_id, platform, handle_or_url.

remove_socialPOST .../ops/remove_social (write)

Detach a social profile. Requires entity_id, platform, handle_or_url.

find_by_socialPOST .../ops/find_by_social (read)

Lookup an entity by social profile (platform + handle/URL). Requires platform, handle_or_url. Returns { entity, social }.

Messaging (Person only)

All messaging verbs reject with 400 when the parent has kind=Company.

add_messagingPOST .../ops/add_messaging (write)

Attach a messaging handle (WhatsApp, Telegram, Signal, Matrix, Discord, Slack, …); idempotent on (entity_id, platform, handle); consent applies. Requires entity_id, platform, handle.

remove_messagingPOST .../ops/remove_messaging (write)

Detach a messaging handle. Requires entity_id, platform, handle.

find_by_messagingPOST .../ops/find_by_messaging (read)

Lookup a Person entity by messaging handle — used by DM send-gates. Requires platform, handle. Returns { entity, messaging }.

Addresses

Both kinds. Keyed by a generated address_id; multiple addresses are allowed.

add_addressPOST .../ops/add_address (write)

Attach a physical address (each add creates a new row). Fields: kind, street1, street2, city, region, postal_code, country (ISO-3166 alpha-2), label. Requires entity_id.

update_addressPOST .../ops/update_address (write)

Update one address by address_id. Requires entity_id, address_id.

remove_addressPOST .../ops/remove_address (write)

Detach an address by address_id. Requires entity_id, address_id.

Quick Start

Create / upsert a Company

POST /api/{circle}/{slug}/ops/upsert
Content-Type: application/json

{
  "kind": "Company",
  "name": "Acme Inc",
  "domain": "acme.example",
  "org_no": "5566778899",
  "industry": "manufacturing",
  "tags": ["customer"]
}

Upsert a Person linked to that Company

POST /api/{circle}/{slug}/ops/upsert
Content-Type: application/json

{
  "kind": "Person",
  "name": "Ada Lovelace",
  "first_name": "Ada",
  "last_name": "Lovelace",
  "primary_company_id": "<company-entity-uuid>",
  "emails": [
    { "address": "ada@acme.example", "label": "work", "primary": true }
  ]
}

Look a recipient up before sending (send-gate pattern)

POST /api/{circle}/{slug}/ops/find_by_email
Content-Type: application/json

{ "address": "ada@acme.example" }

Returns { entity, email } when the address is known to the circle.

Record consent (Person only)

POST /api/{circle}/{slug}/ops/set_email_consent
Content-Type: application/json

{
  "entity_id": "<person-entity-uuid>",
  "address": "ada@acme.example",
  "consent_status": "double_optin",
  "consent_source": "web_form"
}

Common Mistakes

Omitting kind on upsert. kind and name are both required on upsert; an unknown kind rejects with 400. The kind routes per-kind property validation through the generated kind registry.

Sending consent fields to a Company. consent_status / consent_source / consent_evidence are Person-only. Including them on a Company-kind upsert, add_email, or add_phone rejects with 400 — reception-desk handles are impersonal.

Calling messaging or DNC verbs on a Company. add_messaging / remove_messaging / find_by_messaging, set_do_not_contact, verify_email, set_email_consent, verify_phone, and set_phone_consent are Person-only and reject with 400 for kind=Company.

Adding an address when you meant to edit one. Every add_address creates a new row keyed by a generated address_id. Use update_address with that address_id to modify an existing address.

Expecting set_primary_company to model work history. primary_company_id is a cardinality-1 scalar link to one Company; the target must exist with kind=Company. It does not capture role or dates.

Operations

activity

Get /ops/activity | Auth: Read

Get activity events for this element

Scope depends on element capabilities: individual elements query by element_id, project-form elements with activity-scope-members include member activities, circle-level elements with activity-scope-all query the entire circle. Gracefully returns empty list if activities table is missing (old circles).

add_address

Post /ops/add_address | Auth: Write

Attach a physical address to an entity

Every add creates a new row (keyed by generated address_id). Use update_address to modify an existing one. Person kinds: postal | billing | shipping | visiting | residential. Company kinds: registered | billing | visiting | shipping | postal.

add_email

Post /ops/add_email | Auth: Write

Attach an email to an entity (consent fields apply to kind=Person only)

Idempotent on (entity_id, address). Re-adding an existing address merges the provided fields. Consent fields (consent_status, consent_source, consent_evidence) are accepted only when the parent entity has kind=Person; rejected with 400 for kind=Company.

add_messaging

Post /ops/add_messaging | Auth: Write

Person only. Attach a messaging handle (WhatsApp, Telegram, Signal, Matrix, Discord, Slack, …)

Idempotent on (entity_id, platform, handle). Rejects 400 for kind=Company. Consent applies — DMing a WhatsApp handle without opt-in is the same category as cold-emailing.

add_phone

Post /ops/add_phone | Auth: Write

Attach a phone number to an entity (consent fields apply to kind=Person only)

Numbers normalised to E.164 on insert. Idempotent on (entity_id, normalised_number). Consent fields accepted only when parent has kind=Person.

add_social

Post /ops/add_social | Auth: Write

Attach a social profile to an entity

Idempotent on (entity_id, platform, handle_or_url). Platform is an open string. Socials are pointers, not channels — no consent.

attachments

Get /ops/attachments | Auth: Read

List all modifiers and resources attached to this element

Returns both modifiers (policy enforcement) and resources (data injection) with is_modifier flag to distinguish. Items in the generated MODIFIER_TYPES list are modifiers; everything else is a resource. Includes cascade_policy and version pin info.

batch_stats

Get /ops/batch_stats | Auth: Read

Get per-element statistics for all children of this element

Returns per-child stats plus an aggregate. Most meaningful on compound or manifest form elements (repositories, circles, projects); atoms have no children so the result is an empty children array with a zeroed aggregate. Uses efficient GROUP BY SQL. Weighted averages for eval scores.

compose

Post /ops/compose | Auth: Execute

Batch add and remove modifiers on this element in a single call

Declarative composition: add modifiers by ref path (slug or path@version) and remove by attachment ID, all in one atomic call on the target element. Each ‘add’ entry resolves the source element, validates topology, attaches with optional priority and cascade policy. Each ‘remove’ entry deletes the attachment row. Returns a summary of what was added and removed. Example: compose({ add: [{ref: “my-prompt”}, {ref: “rate-limit/api@v2”, priority: 50}], remove: [{attachment_id: “uuid”}] })

context

Get /ops/context | Auth: Read

Get connected elements (graph traversal)

Graph traversal showing all connected elements with their relationship type (contains, contained_by, references, referenced_by, attaches, etc.). Use ?depth=N to control traversal depth (default 1) and ?types=actor,data to filter by element types.

create

Post /ops/create | Auth: Write

Create child element

POST to the parent path — element_type goes in the request body, NOT the URL. Both element_type and slug are required and must be non-empty. Name is derived from slug if omitted. Writes to both Git and PostgreSQL. All elements are stored flat under the circle — no intermediate library wrapper rows.

delete

Post /ops/delete | Auth: Write

Delete an entity and all child rows (GDPR right-to-erasure)

Hard delete — removes the entity row and every email / phone / social / messaging / address child row (CASCADE). Cross-entity links (Person.primary_company_id) become NULL on the dependent side; cascading entity deletion is NOT implied (deleting a Company does not delete its Persons).

disable

Post /ops/disable | Auth: Admin

Disable element (hides and prevents use)

Idempotent — safe to call on already-disabled elements. Optionally pass a reason string. Disabled elements cannot be invoked or executed. Inverse of enable.

enable

Post /ops/enable | Auth: Admin

Enable element (makes usable and visible)

Idempotent — safe to call on already-enabled elements. Transitions element to ready/enabled state. Cannot enable deleted elements. Inverse of disable.

export_bundle

Get /ops/export/bundle | Auth: Read

Export element as downloadable git bundle

On non-root-namespace elements, returns a binary git bundle. On root-namespace (circle) elements, dispatch hands off to the circle’s own export_bundle op, which returns a multi-element JSON envelope with one base64 bundle per child element — this is intentional, not an error.

find_by_email

Post /ops/find_by_email | Auth: Read

Lookup an entity by email address — used by outbound send-gates

Returns the matched entity (with all children) plus the specific email row that matched. Outbound email’s unknown-recipient brake calls this to decide whether an address is known to the circle. Matches across both Person and Company kinds (an inbound info@ lands on the Company that owns the reception-desk email).

find_by_messaging

Post /ops/find_by_messaging | Auth: Read

Lookup a Person entity by messaging handle — used by DM send-gates

find_by_phone

Post /ops/find_by_phone | Auth: Read

Lookup an entity by phone number — used by SMS / voice send-gates

Number normalised to E.164 before lookup. Returns the matched entity (with all children) + the phone row that matched. Spans Person and Company kinds.

find_by_social

Post /ops/find_by_social | Auth: Read

Lookup an entity by social profile (platform + handle/URL)

get

Post /ops/get | Auth: Read

Fetch an entity with all child groupings (emails, phones, socials, messaging, addresses)

import_bundle

Post /ops/import/bundle | Auth: Write

Import git bundle into element

Accepts a base64-encoded git bundle in the JSON bundle_base64 field. Use overwrite=true to replace existing elements with same slug (default skips duplicates). Imported elements get new UUIDs. Returns counts of imported/skipped elements and any errors.

intention

Get /ops/intention | Auth: Read

Get element intention with full inheritance chain

Returns three levels: direct (this element’s intention), inherited (from category and root), and resolved (final merged intention). Useful for understanding an element’s purpose in context of its hierarchy.

list

Post /ops/list | Auth: Read

Page through entities — optionally filtered by kind, primary_company_id, or tag

Without filters, returns every entity in the circle newest-first. kind filters to one kind (e.g. all Persons, all Companies). primary_company_id filters Persons by their linked Company. tag filters to entities carrying a specific tag. All filters AND together.

promote

Post /ops/promote | Auth: Admin

Promote element configuration to a target environment

Only for manifest-form elements (projects). Environments advance: dev → demo → live. dev→demo requires member+ role, demo→live requires admin. Freezes member versions at promotion time (creates snapshot). Persists environment config to spec.environments.

readme

Get /ops/readme | Auth: Read

Get element README.md content

Reads README.md from the element’s git repository. Returns empty content (not an error) if no README exists. Always returns markdown format.

readme_update

Post /ops/readme_update | Auth: Write

Update element README.md content

Creates or overwrites README.md in the element’s git repo. Commits to the draft branch. Content must be provided as a markdown string.

remove-modifier

Post /ops/remove-modifier | Auth: Execute

Remove an attached modifier from this element by attachment ID

Removes a modifier/resource attachment by its row ID. The ID comes from the attachments or context API. This is the reverse of attach — called on the target element, not the source.

remove_address

Post /ops/remove_address | Auth: Write

Detach an address from an entity by address_id

remove_email

Post /ops/remove_email | Auth: Write

Detach an email address from an entity

remove_messaging

Post /ops/remove_messaging | Auth: Write

Person only. Detach a messaging handle from an entity

remove_phone

Post /ops/remove_phone | Auth: Write

Detach a phone number from an entity

remove_social

Post /ops/remove_social | Auth: Write

Detach a social profile from an entity

restore

Post /ops/restore | Auth: Admin

Restore element to a specific version

Automatically snapshots the current state before restoring (creates a ‘Before restore to vN’ version entry). Writes restored spec to git as .triform/spec.yaml. Git failures warn but don’t fail the operation — DB state is authoritative. Cannot restore deleted elements.

schema

Get /ops/schema | Auth: Read

Get element input/output schema (MCP tools/list compatible)

Returns type-level port schemas from the TypeRegistry — not instance-specific overrides. Includes direction (input/output), required flag, and JSON schema per port. Useful for understanding what data an element accepts and produces.

search

Post /ops/search | Auth: Read

Free-text search across name + child handles (email, phone, domain, messaging)

Substring match across the entity row’s name fields, plus any email address, phone number, messaging handle, social handle/URL, or Company domain attached. Optional kind filter narrows the search. Returns matched entities with all children populated.

set_do_not_contact

Post /ops/set_do_not_contact | Auth: Write

Person only. Set or clear the master do-not-contact switch

DNC sits above per-handle consent: when true, every send-gate (email, SMS, voice, WhatsApp, …) refuses outbound regardless of whether individual child rows are opted in. Records reason + source + timestamp for GDPR Art. 21 evidence. Rejects with 400 when the target entity has kind=Company.

set_email_consent

Post /ops/set_email_consent | Auth: Write

Person only. Record consent state for a specific email address

Writes consent_status / consent_source / consent_evidence / consent_at (= now). Rejects with 400 when the parent entity has kind=Company. consent_evidence is GDPR Art. 7 structured proof.

set_phone_consent

Post /ops/set_phone_consent | Auth: Write

Person only. Record consent state for a specific phone number

set_primary_company

Post /ops/set_primary_company | Auth: Write

Person only. Set or clear primary_company_id (scalar Person→Company shortcut)

Replaces today’s contacts.link_organization / unlink_organization pair. Pass primary_company_id to set; omit it (or pass null) to clear. The target must exist with kind=Company. Person→Company is cardinality-1 at the scalar layer; cardinality-N work history will live in data/relationship from Phase 6.

source

Get /ops/source | Auth: Read

Get any file’s content from the element’s git repository

Reads an arbitrary file from the element’s CAS-backed git tree by its relative path. Same store as readme, just generalized. Path safety: rejects .. traversal, leading /, and null bytes. Use this to view main.py for action elements, asset files for SPAs, etc. Returns empty content (not an error) if the file doesn’t exist.

source_branches

Get /ops/source/branches | Auth: Read

List Source branches for this element

Returns the standard draft/demo/live Source branches, their current commits, and promotion relationships. Use GET /api/{element_path}/ops/source/branches.

source_fixtures

Post /ops/source/fixtures | Auth: Write

Dry-run or apply approved Source seed fixtures

Scans .triform/fixtures/ manifests from the addressed data element Source repo. Defaults to dry_run=true and never imports live runtime data. Apply requires dry_run=false plus confirm=true and dispatches approved records through existing generated element ops.

source_promote

Post /ops/source/promote | Auth: Write

Promote Source branch forward

Promotes draft to demo or demo to live through the generated element op path. Direct Git pushes to demo/live are blocked by Source policy.

source_repair

Post /ops/source/repair | Auth: Write

Inspect or repair the element Source index

Runs Source repair through the element operation path. Defaults to dry_run=true; set dry_run=false only after reviewing a dry-run report.

source_status

Get /ops/source/status | Auth: Read

Get Source control status for this element

Returns the branch-aware clone URL, checkout commands, current draft commit, child source-link count, portable export summary, Source health, warnings, and auth hints for the addressed element. Use the element-first path: GET /api/{element_path}/ops/source/status.

source_validate

Post /ops/source/validate | Auth: Read

Validate Source branch contents

Validates a Source branch before accepting local Git workflow changes or promotion. Defaults to branch=draft and rejects runtime data, generated output, secret material, and unreadable CAS refs.

stats

Post /ops/stats | Auth: Read

Aggregate counts — per kind, per consent status, recently-engaged

tag

Post /ops/tag | Auth: Write

Add a tag to an entity

tree

Get /ops/tree | Auth: Read

Get the element’s position in the graph — ancestors, children, references, and subtree statistics

Uses per-circle ElementGraph cache for O(1) lookups. Returns ancestors (containment chain), children (direct), members (references), referenced_by (reverse refs), attachments, and subtree stats. Default depth is 3, max is 10. Pass ?include_metadata=true for name/state on each node.

untag

Post /ops/untag | Auth: Write

Remove a tag from an entity

update

Patch /ops/update | Auth: Write

Update element

Partial update — send only the fields you want to change. spec, name, and intention are all independently optional. spec MUST be a JSON object when present; deep-merged into the existing spec by default. Empty {"spec":{}} preserves existing spec content but still records a new version (no-op for content, not for version state). To clear/replace the entire spec wholesale send {"spec":{...},"deep":false}. List-typed spec fields use replace semantics (the patch list replaces the existing list, no array merging). Coordinates Git + DB writes. Slug cannot be changed after creation.

update_address

Post /ops/update_address | Auth: Write

Update one address on an entity by address_id

update_meta

Patch /ops/update_meta | Auth: Write

Update element metadata (lightweight merge — does NOT bump version or snapshot spec)

Shallow JSONB merge into element.meta. Top-level keys in the provided value replace existing meta values; other keys are preserved. Used for UI metadata like canvas positions, panel state, viewer preferences. Wire-shape op_name is update_meta (distinct from update) so SSE subscribers + the cache auto-invalidator can distinguish lightweight metadata changes from spec edits without inspecting the payload. The MutatingElementStore wrapper stamps this op_name on the lifecycle event emitted by update_element_meta storage calls.

upsert

Post /ops/upsert | Auth: Write

Create or update an entity — kind-aware property validation; merges child groupings by natural key

Idempotent on id (when provided) or on the kind’s declared dedup keys (see kinds/.yaml — Person: email/phone/messaging match; Company: org_no/domain match). The kind field is REQUIRED and routes per-kind property validation through the generated kind registry. Unknown kinds reject with 400. Child arrays UPSERT their rows — existing rows matching on natural key are merged; new rows are inserted. Rows NOT named in the arrays are preserved. Tags union with existing. Custom JSON merges shallowly at top level. Company-kind upserts that include consent_* fields on child rows reject with 400 (consent doesn’t apply to companies — per design §3.1, reception-desk handles are impersonal). Person-kind upserts may set primary_company_id to scalar-link a Person to a Company entity (replaces today’s contacts.organization_id). The referenced entity must exist with kind=Company.

verify_email

Post /ops/verify_email | Auth: Write

Person only. Mark an email as verified (double-opt-in confirmed)

Flips verified=true and, when the email was previously at soft_optin, upgrades to double_optin. Rejects with 400 when the parent entity has kind=Company.

verify_phone

Post /ops/verify_phone | Auth: Write

Person only. Mark a phone number as verified (SMS/call confirmation)

version

Get /ops/version | Auth: Read

Get current version or full history

Returns current version by default. Pass ?history=true for full version history (up to ?limit=N, default 50). Versions are backed by the element_versions table. Every spec update creates a new version entry.

Observability

Defined for this element

Metrics

  • entities_inserted_count
  • entities_updated_count
  • entities_deleted_count
  • entities_query_count
  • entities_query_latency_ms

Events

  • entity.entity.inserted
  • entity.entity.updated
  • entity.entity.deleted
  • entity.query.executed

Pricing / cost

Platform default

Operation costs

  • create: free
  • update: free
  • delete: free
  • get: free
  • list: free
  • invoke: 10000 micro-AU
  • tool_use: free