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.
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_idon a Person row mirrors today’scontacts.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
| Property | Value |
|---|---|
| Type | entity |
| Category | data |
| Form | atom |
| Symbol | En |
| Icon | hub / #7C3AED |
| Open mode | workbench (workbench-props) |
Runtime
| Field | Value |
|---|---|
storage_backend | postgres |
has_data | true |
wirable | false |
llm_role | data |
llm_priority | 7 |
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:
| Kind | Label (singular / plural) | Child types | Per-handle consent |
|---|---|---|---|
Person | Contact / Contacts | email, phone, social, messaging, address | yes (email, phone, messaging) |
Company | Organization / Organizations | email, phone, social, address | no |
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
upsert — POST .../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.
get — POST .../ops/get (read)
Fetch one entity with all child groupings (emails, phones, socials, messaging, addresses). Requires id.
list — POST .../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 }.
search — POST .../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.
delete — POST .../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.
tag — POST .../ops/tag (write)
Add a tag to an entity. Requires id, tag.
untag — POST .../ops/untag (write)
Remove a tag from an entity. Requires id, tag.
set_primary_company — POST .../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_contact — POST .../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.
stats — POST .../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_email — POST .../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_email — POST .../ops/remove_email (write)
Detach an email address. Requires entity_id, address.
find_by_email — POST .../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_email — POST .../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_consent — POST .../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_phone — POST .../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_phone — POST .../ops/remove_phone (write)
Detach a phone number. Requires entity_id, number.
find_by_phone — POST .../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_phone — POST .../ops/verify_phone (write)
Person only. Mark a phone number verified (SMS/call confirmation). Requires entity_id, number.
set_phone_consent — POST .../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_social — POST .../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_social — POST .../ops/remove_social (write)
Detach a social profile. Requires entity_id, platform, handle_or_url.
find_by_social — POST .../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_messaging — POST .../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_messaging — POST .../ops/remove_messaging (write)
Detach a messaging handle. Requires entity_id, platform, handle.
find_by_messaging — POST .../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_address — POST .../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_address — POST .../ops/update_address (write)
Update one address by address_id. Requires entity_id, address_id.
remove_address — POST .../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.
kindfilters to one kind (e.g. all Persons, all Companies).primary_company_idfilters Persons by their linked Company.tagfilters 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
kindfilter 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_idto 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 viewmain.pyfor 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, andintentionare all independently optional.specMUST 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 fromupdate) 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 byupdate_element_metastorage 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 kindfield 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 setprimary_company_idto scalar-link a Person to a Company entity (replaces today’scontacts.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