#Entities & Data Model
Entities are the records your application works with — a vessel, a task, a customer. Every entity belongs to an entity type, which pins its shape to a JSON schema and declares which attributes are queryable. Entities connect to one another through relationship rules (e.g. Project → Task). See Core Concepts for the mental model.
Authenticate with a Personal Access Token (HTTP Basic auth) — enter it once in the Authorize bar at the top of this page and every Try it below uses it. Each example body is pre-filled; the console targets staging by default. Full schemas and field docs are in the linked API reference.
#Build a data model
The flow is always the same: schema → entity type → relationship rule → entities → links.
#1. Define a JSON schema
A schema describes an entity's attributes. Reuse one schema across many types.
POST/schema/schemasRegister a JSON schemaAPI docs ↗Try it
#2. Create an entity type
The type binds a name to a schema and declares config.identifier_key — the
attribute path(s) that derive an entity's stable business key, computed
server-side on every write — and config.indexed_attributes — only indexed
attributes are filterable/sortable in queries (querying an un-indexed attribute
returns 400 INVALID_FILTER). A type is draft until you activate it; only
active types accept entities.
POST/entity/entity-typesCreate an entity typeAPI docs ↗Try it
#3. Add a relationship rule
A rule declares a directed connection between two types and its cardinality
(max_from_cardinality / max_to_cardinality; null = unlimited). Cardinality
is enforced when links are written.
POST/entity/relationship-rulesCreate a relationship ruleAPI docs ↗Try it
#4. Create entities
attributes are validated against the type's schema. identifier_key is never
sent by the caller — it's computed server-side from config.identifier_key
(here, the key attribute) and returned on every response. A create whose
computed identifier_key collides with a live entity returns 409 — use the
upsert path instead.
POST/entity/entitiesCreate an entityAPI docs ↗Try it
#5. Link entities
Link two existing entities via a rule. on_cardinality_violation chooses what
happens when the link would exceed a rule's cap: raise (default, abort) or
trim (soft-delete the oldest edge to fit).
POST/entity/relationshipsLink two entitiesAPI docs ↗Try it
#Read & query
POST /entity/query is the read surface for anything non-trivial. The body is a
filter tree plus sort, pagination, and relationship includes. Cross-tenant rows
you can see through a share come back tagged source_type: "shared".
POST/entity/queryQuery entitiesAPI docs ↗Try it
- Filter operators:
eq/neq,gt/gte/lt/lte,like/ilike,is,in,cs(array contains-all),ov(array overlaps), combined withand/or/not. - Path conventions:
["name"](a system field);["attributes", "x"](an indexed attribute — un-indexed paths are rejected);["relationships", ruleName, "attributes", "x"](filter through a relationship). - Pagination is keyset by default (
cursor); offset (page/limit) is the escape hatch.includeloads one hop of related entities by rule name;POST /entity/query/distinctreturns distinct values for a path.
#Deep relationship reads — /query-graph
/entity/query reads one hop deep. For a whole related subtree in a single
call, use POST /entity/query-graph — the same type_name / filter /
sort / pagination shape, plus a nested include tree (each entry can itself
carry include, filter, sort, limit, to any depth up to a server cap).
Instead of a row-per-parent shape, the response is one deduplicated graph:
roots (matching entity ids), nodes (every reachable entity, keyed by id —
a node shared by two branches is serialized once), and edges. This is the
REST-native way to do a graph-shaped read without GraphQL, and it's the surface
most integrations reach for by default.
POST/entity/query-graphQuery a graph of entities (deep hydration)API docs ↗Try it
If the node budget constrains a deeply-nested branch, page_info.truncated is
true and page_info.truncations reports which branch and at what depth —
narrow that branch's limit, prune it, or paginate the roots and re-issue.
For a GraphQL surface over the same access rules (selective fields, Relay
Connections, codegen) see
Querying & GraphQL. The GraphQL playground also
has a converter that turns a query into a workflow QueryEntity step.
#Bulk upsert & import
POST /entity/entities/upsert writes a large batch in one transaction (capped
per request — see the reference). Each row matches an existing entity on its
computed identifier_key (derived from attributes.key per the type's
config above) — never a caller-supplied identifier_key. Useful options:
skip_unresolved_relationships (skip + log an inline relationship whose
target_identifier doesn't resolve, instead of failing the batch),
merge_on_conflict / merge_cardinality (fold a colliding
identifier_key into the surviving entity), and suppress_events.
POST/entity/entities/upsertBulk upsert entitiesAPI docs ↗Try it
For files, POST /entity/entities/import streams a CSV (an X-Import-Config
header carries the column mapping + on_conflict strategy); …/export streams
the inverse.
#Update, patch & delete
PUT replaces an entity; PATCH …/attributes deep-merges a partial attribute
update; DELETE soft-deletes the entity and cascades to its relationship
instances. Updates accept the same merge_on_conflict / merge_cardinality
options as upsert.
PATCH/entity/entities/{id}/attributesPatch entity attributes (deep-merge)API docs ↗Try it
#Attach files (artifacts)
Files attach to an entity via a two-step flow: request an upload URL, PUT the bytes to it, then confirm. Artifacts are encrypted at rest and access is gated by the same permissions as the entity.
POST/entity/entities/{id}/artifacts/upload-urlRequest an artifact upload URLAPI docs ↗Try it
#Sharing & access
Sharing your data model and rows with other tenants — definition shares, data shares, and grants — is its own topic. See Shares & Grants.
#API reference
Every entity and schema operation, with full request/response schemas and an interactive console, is in the API reference.